diff --git a/assets/src/scripts/components/hydrograph/index.spec.js b/assets/src/scripts/components/hydrograph/index.spec.js
index 3524b73a981d916338cc6c9e74534a4a6dedc516..bd11ab013106bed20757ac80630f0ad0db8e2a9a 100644
--- a/assets/src/scripts/components/hydrograph/index.spec.js
+++ b/assets/src/scripts/components/hydrograph/index.spec.js
@@ -1,7 +1,5 @@
-import { select, selectAll } from 'd3-selection';
-import { provide } from '../../lib/redux';
-import { attachToNode, timeSeriesLegend } from './index';
-import { drawTimeSeriesGraph } from './time-series-graph';
+import { select } from 'd3-selection';
+import { attachToNode } from './index';
 import { Actions, configureStore } from '../../store';
 
 
@@ -151,7 +149,7 @@ const TEST_STATE = {
 };
 
 
-describe('Hydrograph charting module', () => {
+describe('Loading indicators and data alerts', () => {
     let graphNode;
 
     beforeEach(() => {
@@ -173,221 +171,7 @@ describe('Hydrograph charting module', () => {
         select('#hydrograph').remove();
     });
 
-    it('empty graph displays warning', () => {
-        attachToNode({}, graphNode, {});
-        expect(graphNode.innerHTML).toContain('No data is available');
-    });
-
-    it('single data point renders', () => {
-        const store = configureStore(TEST_STATE);
-        select(graphNode)
-            .call(provide(store))
-            .call(drawTimeSeriesGraph);
-        let svgNodes = graphNode.getElementsByTagName('svg');
-        expect(svgNodes.length).toBe(1);
-        expect(graphNode.innerHTML).toContain('hydrograph-container');
-    });
-
-    describe('container display', () => {
-
-        it('should not be hidden tag if there is data', () => {
-            const store = configureStore(TEST_STATE);
-            select(graphNode)
-                .call(provide(store))
-                .call(drawTimeSeriesGraph);
-            expect(select('#hydrograph').attr('hidden')).toBeNull();
-        });
-
-        it('should have a style tag if there is no data', () => {
-            const store = configureStore({series: {timeSeries: {}}});
-            select(graphNode)
-                .call(provide(store))
-                .call(drawTimeSeriesGraph);
-        });
-    });
-
-    describe('SVG has been made accessibile', () => {
-        let svg;
-        beforeEach(() => {
-            const store = configureStore(TEST_STATE);
-            select(graphNode)
-                .call(provide(store))
-                .call(drawTimeSeriesGraph);
-            svg = select('svg');
-        });
-
-        it('title and desc attributes are present', function() {
-            const descText = svg.select('desc').html();
-
-            expect(svg.select('title').html()).toEqual('Test title for 00060');
-            expect(descText).toContain('Test description for 00060');
-            expect(descText).toContain('3/23/2018');
-            expect(descText).toContain('3/30/2018');
-            expect(svg.attr('aria-labelledby')).toContain('title');
-            expect(svg.attr('aria-describedby')).toContain('desc');
-        });
-
-        it('svg should be focusable', function() {
-            expect(svg.attr('tabindex')).toBe('0');
-        });
-    });
-
-    describe('SVG contains the expected elements', () => {
-        /* eslint no-use-before-define: 0 */
-        let store;
-        beforeEach(() => {
-            store = configureStore({
-                ...TEST_STATE,
-                series: {
-                    ...TEST_STATE.series,
-                    timeSeries: {
-                        ...TEST_STATE.series.timeSeries,
-                        '00060:current': {
-                            ...TEST_STATE.series.timeSeries['00060:current'],
-                            startTime: 1514926800000,
-                            endTime: 1514930400000,
-                            points: [{
-                                dateTime: 1514926800000,
-                                value: 10,
-                                qualifiers: ['P']
-                            }, {
-                                dateTime: 1514930400000,
-                                value: null,
-                                qualifiers: ['P', 'FLD']
-                            }]
-                        }
-                    }
-                },
-                timeSeriesState: {
-                    showSeries: {
-                        current: true,
-                        compare: true,
-                        median: true
-                    },
-                    currentVariableID: '45807197',
-                    currentDateRange: 'P7D',
-                    currentMethodID: 'method1',
-                    loadingTSKeys: []
-                },
-                ui: {
-                    windowWidth: 400,
-                    width: 400
-                }
-
-            });
-
-            attachToNode(store, graphNode, {siteno: '123456788'});
-        });
-
-        it('should render the correct number of svg nodes', () => {
-            // one main hydrograph, legend and two sparklines
-            expect(selectAll('svg').size()).toBe(4);
-        });
-
-        it('should have a title div', () => {
-            const titleDiv = selectAll('.time-series-graph-title');
-            expect(titleDiv.size()).toBe(1);
-            expect(titleDiv.text()).toEqual('Test title for 00060, method description');
-        });
-
-        it('should have a defs node', () => {
-            expect(selectAll('defs').size()).toBe(1);
-            expect(selectAll('defs mask').size()).toBe(1);
-            expect(selectAll('defs pattern').size()).toBe(2);
-        });
-
-        it('should render time series data as a line', () => {
-            // There should be one segment per time-series. Each is a single
-            // point, so should be a circle.
-            expect(selectAll('.hydrograph-svg .line-segment').size()).toBe(2);
-        });
-
-        it('should render a rectangle for masked data', () => {
-            expect(selectAll('.hydrograph-svg g.current-mask-group').size()).toBe(1);
-        });
-
-        it('should have a point for the median stat data with a label', () => {
-            expect(selectAll('#median-points path').size()).toBe(1);
-            expect(selectAll('#median-points text').size()).toBe(0);
-        });
-
-        it('should have tooltips for the select series table', () => {
-            // one for each of the two parameters
-            expect(selectAll('table .tooltip-item').size()).toBe(2);
-        });
-
-        it('should not have tooltips for the select series table when the screen is large', () => {
-            store.dispatch(Actions.resizeUI(800, 800));
-            expect(selectAll('table .tooltip-table').size()).toBe(0);
-        });
-    });
-
-    //TODO: Consider adding a test which checks that the y axis is rescaled by
-    // examining the contents of the text labels.
-
-    describe('legends should render', () => {
-        let store;
-
-        beforeEach(() => {
-            store = configureStore(TEST_STATE);
-            select(graphNode)
-                .call(provide(store))
-                .call(timeSeriesLegend);
-        });
-
-        it('Should have 6 legend markers', () => {
-            expect(selectAll('.legend g').size()).toBe(6);
-            expect(selectAll('.legend g line.median-step').size()).toBe(1);
-        });
-
-        it('Should have four legend markers after the compare time series is removed', () => {
-            store.dispatch(Actions.toggleTimeSeries('compare', false));
-            expect(selectAll('.legend g').size()).toBe(4);
-        });
-
-        it('Should have two legend marker after the compare and median time series are removed', () => {
-            store.dispatch(Actions.toggleTimeSeries('compare', false));
-            store.dispatch(Actions.toggleTimeSeries('median', false));
-            expect(selectAll('.legend g').size()).toBe(2);
-        });
-    });
-
-    describe('compare line', () => {
-
-        let store;
-        beforeEach(() => {
-            store = configureStore(TEST_STATE);
-            attachToNode(store, graphNode, {siteno: '12345678'});
-        });
-
-        it('Should render one lines', () => {
-            expect(selectAll('#ts-compare-group .line-segment').size()).toBe(1);
-        });
-
-        it('Should remove the lines when removing the compare time series', () => {
-            store.dispatch(Actions.toggleTimeSeries('compare', false));
-            expect(selectAll('#ts-compare-group .line-segment').size()).toBe(0);
-        });
-    });
-
-    describe('median lines', () => {
-        let store;
-        beforeEach(() => {
-            store = configureStore(TEST_STATE);
-            attachToNode(store, graphNode, {siteno: '12345678'});
-        });
-
-        it('Should render one lines', () => {
-            expect(selectAll('#median-points .median-data-series').size()).toBe(1);
-        });
-
-        it('Should remove the lines when removing the median statistics data', () => {
-            store.dispatch(Actions.toggleTimeSeries('median', false));
-            expect(selectAll('#median-points .median-data-series').size()).toBe(0);
-        });
-    });
-
-    describe('hiding/show provisional alert', () => {
+    describe('hiding/showing provisional alert', () => {
 
         it('Expects the provisional alert to be visible when time series data is provided', () => {
             let store = configureStore(TEST_STATE);
diff --git a/assets/src/scripts/components/hydrograph/time-series-graph.spec.js b/assets/src/scripts/components/hydrograph/time-series-graph.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..8801c0e8efd3be439c23416c002bbe7a5f821a8a
--- /dev/null
+++ b/assets/src/scripts/components/hydrograph/time-series-graph.spec.js
@@ -0,0 +1,388 @@
+import { select, selectAll } from 'd3-selection';
+import {attachToNode, timeSeriesLegend} from './index';
+import {Actions, configureStore} from '../../store';
+import {provide} from '../../lib/redux';
+import {drawTimeSeriesGraph} from './time-series-graph';
+
+
+const TEST_STATE = {
+    series: {
+        timeSeries: {
+            '00010:current': {
+                points: [{
+                    dateTime: 1514926800000,
+                    value: 4,
+                    qualifiers: ['P']
+                }],
+                method: 'method1',
+                tsKey: 'current:P7D',
+                variable: '45807190'
+            },
+            '00060:current': {
+                points: [{
+                    dateTime: 1514926800000,
+                    value: 10,
+                    qualifiers: ['P']
+                }],
+                method: 'method1',
+                tsKey: 'current:P7D',
+                variable: '45807197'
+            },
+            '00060:compare': {
+                points: [{
+                    dateTime: 1514926800000,
+                    value: 10,
+                    qualifiers: ['P']
+                }],
+                method: 'method1',
+                tsKey: 'compare:P7D',
+                variable: '45807197'
+            }
+        },
+        timeSeriesCollections: {
+            'coll1': {
+                variable: '45807197',
+                timeSeries: ['00060:current']
+            },
+            'coll2': {
+                variable: '45807197',
+                timeSeries: ['00060:compare']
+            },
+            'coll3': {
+                variable: '45807197',
+                timeSeries: ['00060:median']
+            },
+            'coll4': {
+                variable: '45807190',
+                timeSeries: ['00010:current']
+            }
+        },
+        queryInfo: {
+            'current:P7D': {
+                notes: {
+                    'filter:timeRange':  {
+                        mode: 'PERIOD',
+                        periodDays: 7
+                    },
+                    requestDT: 1522425600000
+                }
+            }
+        },
+        requests: {
+            'current:P7D': {
+                timeSeriesCollections: ['coll1']
+            },
+            'compare:P7D': {
+                timeSeriesCollections: ['coll2', 'col4']
+            }
+        },
+        variables: {
+            '45807197': {
+                variableCode: {
+                    value: '00060'
+                },
+                oid: '45807197',
+                variableName: 'Test title for 00060',
+                variableDescription: 'Test description for 00060',
+                unit: {
+                    unitCode: 'unitCode'
+                }
+            },
+            '45807190': {
+                variableCode: {
+                    value: '00010'
+                },
+                oid: '45807190',
+                unit: {
+                    unitCode: 'unitCode'
+                }
+            }
+        },
+        methods: {
+            'method1': {
+                methodDescription: 'method description'
+            }
+        }
+    },
+    statisticsData : {
+        median: {
+            '00060': {
+                '1234': [
+                    {
+                        month_nu: '2',
+                        day_nu: '20',
+                        p50_va: '40',
+                        begin_yr: '1970',
+                        end_yr: '2017',
+                        loc_web_ds: 'This method'
+                    }, {
+                        month_nu: '2',
+                        day_nu: '21',
+                        p50_va: '41',
+                        begin_yr: '1970',
+                        end_yr: '2017',
+                        loc_web_ds: 'This method'
+                    }, {
+                        month_nu: '2',
+                        day_nu: '22',
+                        p50_va: '42',
+                        begin_yr: '1970',
+                        end_yr: '2017',
+                        loc_web_ds: 'This method'
+                    }
+                ]
+            }
+        }
+    },
+    timeSeriesState: {
+        currentVariableID: '45807197',
+        currentDateRange: 'P7D',
+        requestedTimeRange: null,
+        showSeries: {
+            current: true,
+            compare: true,
+            median: true
+        },
+        loadingTSKeys: []
+    },
+    ui: {
+        width: 400
+    }
+};
+
+describe('time series graph', () => {
+    let graphNode;
+
+    beforeEach(() => {
+        let body = select('body');
+        let component = body.append('div')
+            .attr('id', 'hydrograph');
+        component.append('div').attr('class', 'loading-indicator-container');
+        component.append('div').attr('class', 'graph-container');
+        component.append('div').attr('class', 'select-time-series-container');
+        component.append('div').attr('class', 'provisional-data-alert');
+
+        graphNode = document.getElementById('hydrograph');
+
+        jasmine.Ajax.install();
+    });
+
+    afterEach(() => {
+        jasmine.Ajax.uninstall();
+        select('#hydrograph').remove();
+    });
+
+    it('empty graph displays warning', () => {
+        attachToNode({}, graphNode, {});
+        expect(graphNode.innerHTML).toContain('No data is available');
+    });
+
+    it('single data point renders', () => {
+        const store = configureStore(TEST_STATE);
+        select(graphNode)
+            .call(provide(store))
+            .call(drawTimeSeriesGraph);
+        let svgNodes = graphNode.getElementsByTagName('svg');
+        expect(svgNodes.length).toBe(1);
+        expect(graphNode.innerHTML).toContain('hydrograph-container');
+    });
+
+    describe('container display', () => {
+
+        it('should not be hidden tag if there is data', () => {
+            const store = configureStore(TEST_STATE);
+            select(graphNode)
+                .call(provide(store))
+                .call(drawTimeSeriesGraph);
+            expect(select('#hydrograph').attr('hidden')).toBeNull();
+        });
+
+        it('should have a style tag if there is no data', () => {
+            const store = configureStore({series: {timeSeries: {}}});
+            select(graphNode)
+                .call(provide(store))
+                .call(drawTimeSeriesGraph);
+        });
+    });
+
+    describe('SVG has been made accessibile', () => {
+        let svg;
+        beforeEach(() => {
+            const store = configureStore(TEST_STATE);
+            select(graphNode)
+                .call(provide(store))
+                .call(drawTimeSeriesGraph);
+            svg = select('svg');
+        });
+
+        it('title and desc attributes are present', function() {
+            const descText = svg.select('desc').html();
+
+            expect(svg.select('title').html()).toEqual('Test title for 00060');
+            expect(descText).toContain('Test description for 00060');
+            expect(descText).toContain('3/23/2018');
+            expect(descText).toContain('3/30/2018');
+            expect(svg.attr('aria-labelledby')).toContain('title');
+            expect(svg.attr('aria-describedby')).toContain('desc');
+        });
+
+        it('svg should be focusable', function() {
+            expect(svg.attr('tabindex')).toBe('0');
+        });
+    });
+
+    describe('SVG contains the expected elements', () => {
+        /* eslint no-use-before-define: 0 */
+        let store;
+        beforeEach(() => {
+            store = configureStore({
+                ...TEST_STATE,
+                series: {
+                    ...TEST_STATE.series,
+                    timeSeries: {
+                        ...TEST_STATE.series.timeSeries,
+                        '00060:current': {
+                            ...TEST_STATE.series.timeSeries['00060:current'],
+                            startTime: 1514926800000,
+                            endTime: 1514930400000,
+                            points: [{
+                                dateTime: 1514926800000,
+                                value: 10,
+                                qualifiers: ['P']
+                            }, {
+                                dateTime: 1514930400000,
+                                value: null,
+                                qualifiers: ['P', 'FLD']
+                            }]
+                        }
+                    }
+                },
+                timeSeriesState: {
+                    showSeries: {
+                        current: true,
+                        compare: true,
+                        median: true
+                    },
+                    currentVariableID: '45807197',
+                    currentDateRange: 'P7D',
+                    currentMethodID: 'method1',
+                    loadingTSKeys: []
+                },
+                ui: {
+                    windowWidth: 400,
+                    width: 400
+                }
+
+            });
+
+            attachToNode(store, graphNode, {siteno: '123456788'});
+        });
+
+        it('should render the correct number of svg nodes', () => {
+            // one main hydrograph, legend and two sparklines
+            expect(selectAll('svg').size()).toBe(4);
+        });
+
+        it('should have a title div', () => {
+            const titleDiv = selectAll('.time-series-graph-title');
+            expect(titleDiv.size()).toBe(1);
+            expect(titleDiv.text()).toEqual('Test title for 00060, method description');
+        });
+
+        it('should have a defs node', () => {
+            expect(selectAll('defs').size()).toBe(1);
+            expect(selectAll('defs mask').size()).toBe(1);
+            expect(selectAll('defs pattern').size()).toBe(2);
+        });
+
+        it('should render time series data as a line', () => {
+            // There should be one segment per time-series. Each is a single
+            // point, so should be a circle.
+            expect(selectAll('.hydrograph-svg .line-segment').size()).toBe(2);
+        });
+
+        it('should render a rectangle for masked data', () => {
+            expect(selectAll('.hydrograph-svg g.current-mask-group').size()).toBe(1);
+        });
+
+        it('should have a point for the median stat data with a label', () => {
+            expect(selectAll('#median-points path').size()).toBe(1);
+            expect(selectAll('#median-points text').size()).toBe(0);
+        });
+
+        it('should have tooltips for the select series table', () => {
+            // one for each of the two parameters
+            expect(selectAll('table .tooltip-item').size()).toBe(2);
+        });
+
+        it('should not have tooltips for the select series table when the screen is large', () => {
+            store.dispatch(Actions.resizeUI(800, 800));
+            expect(selectAll('table .tooltip-table').size()).toBe(0);
+        });
+    });
+
+    //TODO: Consider adding a test which checks that the y axis is rescaled by
+    // examining the contents of the text labels.
+
+    describe('legends should render', () => {
+        let store;
+
+        beforeEach(() => {
+            store = configureStore(TEST_STATE);
+            select(graphNode)
+                .call(provide(store))
+                .call(timeSeriesLegend);
+        });
+
+        it('Should have 6 legend markers', () => {
+            expect(selectAll('.legend g').size()).toBe(6);
+            expect(selectAll('.legend g line.median-step').size()).toBe(1);
+        });
+
+        it('Should have four legend markers after the compare time series is removed', () => {
+            store.dispatch(Actions.toggleTimeSeries('compare', false));
+            expect(selectAll('.legend g').size()).toBe(4);
+        });
+
+        it('Should have two legend marker after the compare and median time series are removed', () => {
+            store.dispatch(Actions.toggleTimeSeries('compare', false));
+            store.dispatch(Actions.toggleTimeSeries('median', false));
+            expect(selectAll('.legend g').size()).toBe(2);
+        });
+    });
+
+    describe('compare line', () => {
+
+        let store;
+        beforeEach(() => {
+            store = configureStore(TEST_STATE);
+            attachToNode(store, graphNode, {siteno: '12345678'});
+        });
+
+        it('Should render one lines', () => {
+            expect(selectAll('#ts-compare-group .line-segment').size()).toBe(1);
+        });
+
+        it('Should remove the lines when removing the compare time series', () => {
+            store.dispatch(Actions.toggleTimeSeries('compare', false));
+            expect(selectAll('#ts-compare-group .line-segment').size()).toBe(0);
+        });
+    });
+
+    describe('median lines', () => {
+        let store;
+        beforeEach(() => {
+            store = configureStore(TEST_STATE);
+            attachToNode(store, graphNode, {siteno: '12345678'});
+        });
+
+        it('Should render one lines', () => {
+            expect(selectAll('#median-points .median-data-series').size()).toBe(1);
+        });
+
+        it('Should remove the lines when removing the median statistics data', () => {
+            store.dispatch(Actions.toggleTimeSeries('median', false));
+            expect(selectAll('#median-points .median-data-series').size()).toBe(0);
+        });
+    });
+});
\ No newline at end of file
diff --git a/assets/src/scripts/index.spec.js b/assets/src/scripts/index.spec.js
index 17818a3567a6c81d254902bceb1004098a87c077..be4f93de33c5d0db4b027610fddf393cf1e1f68d 100644
--- a/assets/src/scripts/index.spec.js
+++ b/assets/src/scripts/index.spec.js
@@ -27,6 +27,7 @@ import './components/hydrograph/method-picker.spec';
 import './components/hydrograph/parameters.spec';
 import './components/hydrograph/scales.spec';
 import './components/hydrograph/time-series.spec';
+import './components/hydrograph/time-series-graph.spec';
 import './components/hydrograph/tooltip.spec';
 import './components/map/flood-slider.spec';
 import './components/map/index.spec';