diff --git a/.babelrc b/.babelrc
new file mode 100644
index 0000000000000000000000000000000000000000..85af630bdd1bcaf0427ba72f929ca6147533793a
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,3 @@
+{
+  "plugins": ["transform-object-rest-spread"]
+}
diff --git a/.eslintrc b/.eslintrc
index 9e0889f17ebb43f973d9d8029520ae21e35cebeb..805e164e747af3e79caccf8fc64a0cd12864877b 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -6,6 +6,9 @@
   },
   "parserOptions": {
     "ecmaVersion": 6,
+    "ecmaFeatures": {
+      "experimentalObjectRestSpread": true
+    },
     "sourceType": "module"
   },
   "plugins": [
diff --git a/assets/src/scripts/accessibility.js b/assets/src/scripts/accessibility.js
index 377e3d660e38584d0c7b10971b0b7ba1b5f3bad0..36583b8286a6370deb2f53a7b712e51e350c577a 100644
--- a/assets/src/scripts/accessibility.js
+++ b/assets/src/scripts/accessibility.js
@@ -1,5 +1,3 @@
-const { select } = require('d3-selection');
-
 /**
  * Adds accessibility attributes to the svg.
  * This was based on the recommendations in this article: https://www.w3.org/WAI/PF/HTML/wiki/Canvas
@@ -8,7 +6,7 @@ const { select } = require('d3-selection');
  * @param {String} description
  * @param {Boolean} isInteractive
  */
-function addSVGAccessibility({svg, title, description, isInteractive}) {
+function addSVGAccessibility(svg, {title, description, isInteractive}) {
     svg.attr('title', title)
         .attr('desc', description)
         .attr('aria-labelledby', 'title desc');
@@ -25,8 +23,10 @@ function addSVGAccessibility({svg, title, description, isInteractive}) {
  * @param {Array} columnNames - array of strings
  * @param {Array} data - array of array of strings
  */
-function addSROnlyTable({container, columnNames, data}) {
-    const table = select(container)
+function addSROnlyTable(container, {columnNames, data}) {
+    container.selectAll('table.usa-sr-only').remove();
+
+    const table = container
         .append('table')
         .attr('class', 'usa-sr-only');
 
@@ -57,4 +57,3 @@ function addSROnlyTable({container, columnNames, data}) {
 }
 
 module.exports = {addSVGAccessibility, addSROnlyTable};
-
diff --git a/assets/src/scripts/accessibility.spec.js b/assets/src/scripts/accessibility.spec.js
index a975e91c98d3f2f9f591deacceb1809860cc83ab..22d33af0e069a069516949d6e5c425c088d93266 100644
--- a/assets/src/scripts/accessibility.spec.js
+++ b/assets/src/scripts/accessibility.spec.js
@@ -14,8 +14,7 @@ describe('svgAccessibility tests', () => {
         });
 
         it('Should add a title, desc, and aria attributes', () => {
-            addSVGAccessibility({
-                svg: svg,
+            addSVGAccessibility(svg, {
                 title: 'This is a title',
                 description: 'This is a description',
                 isInteractive: false
@@ -29,8 +28,7 @@ describe('svgAccessibility tests', () => {
         });
 
         it('Should not add a tabindex if isInteractive is false', () => {
-            addSVGAccessibility({
-                svg: svg,
+            addSVGAccessibility(svg, {
                 title: 'This is a title',
                 description: 'This is a description',
                 isInteractive: false
@@ -39,8 +37,7 @@ describe('svgAccessibility tests', () => {
         });
 
         it('Should add a tabindex if isInteractive is true', () => {
-            addSVGAccessibility({
-                svg: svg,
+            addSVGAccessibility(svg, {
                 title: 'This is a title',
                 description: 'This is a description',
                 isInteractive: true
@@ -49,7 +46,7 @@ describe('svgAccessibility tests', () => {
         });
     });
 
-    describe('addSROnlyTable tests', () => {
+    describe('SROnlyTable tests', () => {
        let container;
        let columnNames = ['Postal Code', 'FIPS', 'Name'];
        let data = [
@@ -61,8 +58,7 @@ describe('svgAccessibility tests', () => {
 
        beforeEach(() => {
            container = select('body').append('div').attr('id', 'test-div');
-           addSROnlyTable({
-               container: document.getElementById('test-div'),
+           addSROnlyTable(select(document.getElementById('test-div')), {
                columnNames: columnNames,
                data: data
            });
@@ -96,4 +92,4 @@ describe('svgAccessibility tests', () => {
        });
 
     });
-});
\ No newline at end of file
+});
diff --git a/assets/src/scripts/components/hydrograph/axes.js b/assets/src/scripts/components/hydrograph/axes.js
index c93f6dd71e4b7cef743d4b44304790e6f4406ff6..655c024514e2b51da57ab4b7bd5512e65a3a128c 100644
--- a/assets/src/scripts/components/hydrograph/axes.js
+++ b/assets/src/scripts/components/hydrograph/axes.js
@@ -2,9 +2,14 @@ const { axisBottom, axisLeft } = require('d3-axis');
 const { format } = require('d3-format');
 const { timeDay } = require('d3-time');
 const { timeFormat } = require('d3-time-format');
+const { createSelector } = require('reselect');
+
+const { WIDTH, HEIGHT, MARGIN } = require('./layout');
+const { xScaleSelector, yScaleSelector } = require('./scales');
 
 const yTickCount = 5;
 
+
 /**
  * Helper function which generates y tick values for a scale
  * @param {Object} yScale - d3 scale
@@ -26,7 +31,7 @@ function yTickValues(yScale) {
  * @param  {Number} yTickSize   Size of inner ticks for the y-axis
  * @return {Object}             {xAxis, yAxis} - D3 Axis
  */
-function createAxes(xScale, yScale, yTickSize) {
+function createAxes({xScale, yScale}, yTickSize) {
     // Create x-axis
     const xAxis = axisBottom()
         .scale(xScale)
@@ -46,33 +51,42 @@ function createAxes(xScale, yScale, yTickSize) {
     return {xAxis, yAxis};
 }
 
-/**
- * Updates/Sets the yAxis.tickValues
- * @param {Object} yAxis - d3 axis
- * @param {Object} yScale - d3 scale
- */
-function updateYAxis(yAxis, yScale) {
-    yAxis.tickValues(yTickValues(yScale));
-}
 
-/**
- * Adds the given axes to a node
- * @param  {Object} plot      Node to append to
- * @param  {Object} xAxis     D3 Axis x-axis
- * @param  {Object} yAxis     D3 Axis y-axis
- * @param  {Object} xLoc      {x, y} location of x-axis
- * @param  {Object} yLoc      {x, y} location of y-axis
- * @param  {Object} yLabelLoc {x, y} location of y-axis label
- * @param  {String} yTitle    y-axis label
- */
-function appendAxes({plot, xAxis, yAxis, xLoc, yLoc, yLabelLoc, yTitle}) {
-    plot.append('g')
+const axesSelector = createSelector(
+    xScaleSelector,
+    yScaleSelector,
+    (state) => state.title,
+    (xScale, yScale, title) => {
+        return {
+            ...createAxes({xScale, yScale}, -WIDTH + MARGIN.right),
+            yTitle: title
+        };
+    }
+);
+
+
+function appendAxes(elem, {xAxis, yAxis, yTitle}) {
+    const xLoc = {
+        x: 0,
+        y: HEIGHT - (MARGIN.top + MARGIN.bottom)
+    };
+    const yLoc = {x: 0, y: 0};
+    const yLabelLoc = {
+        x: HEIGHT / -2 + MARGIN.top,
+        y: -35
+    };
+
+    // Remove existing axes before adding the new ones.
+    elem.selectAll('.x-axis, .y-axis').remove();
+
+    // Add x-axis
+    elem.append('g')
         .attr('class', 'x-axis')
         .attr('transform', `translate(${xLoc.x}, ${xLoc.y})`)
         .call(xAxis);
 
     // Add y-axis and a text label
-    plot.append('g')
+    elem.append('g')
         .attr('class', 'y-axis')
         .attr('transform', `translate(${yLoc.x}, ${yLoc.y})`)
         .call(yAxis)
@@ -86,4 +100,19 @@ function appendAxes({plot, xAxis, yAxis, xLoc, yLoc, yLabelLoc, yTitle}) {
 }
 
 
-module.exports = {createAxes, updateYAxis, appendAxes};
+/**
+ * Adds the given axes to a node
+ * @param  {Object} elem      Node to append to
+ * @param  {Object} xAxis     D3 Axis x-axis
+ * @param  {Object} yAxis     D3 Axis y-axis
+ * @param  {Object} xLoc      {x, y} location of x-axis
+ * @param  {Object} yLoc      {x, y} location of y-axis
+ * @param  {Object} yLabelLoc {x, y} location of y-axis label
+ * @param  {String} yTitle    y-axis label
+ */
+function plotAxes(elem, store) {
+    elem.call(appendAxes, axesSelector(store.getState()));
+}
+
+
+module.exports = {createAxes, appendAxes, axesSelector, plotAxes};
diff --git a/assets/src/scripts/components/hydrograph/axes.spec.js b/assets/src/scripts/components/hydrograph/axes.spec.js
index 6d07d4d2603e5eb3c61d70c7565244b10ed1b241..0ecee414e036c3b5ecb051843af8bbe13e1fc997 100644
--- a/assets/src/scripts/components/hydrograph/axes.spec.js
+++ b/assets/src/scripts/components/hydrograph/axes.spec.js
@@ -1,24 +1,21 @@
-const { createAxes, updateYAxis, appendAxes } = require('./axes');
 const { scaleLinear } = require('d3-scale');
 const { select } = require('d3-selection');
 
+const { createAxes, appendAxes } = require('./axes');
+
 
 describe('Chart axes', () => {
     // xScale is oriented on the left
     const xScale = scaleLinear().range([0, 10]).domain([0, 10]);
     const yScale = scaleLinear().range([0, 10]).domain([0, 10]);
-    const {xAxis, yAxis} = createAxes(xScale, yScale, 100);
+    const {xAxis, yAxis} = createAxes({xScale, yScale}, 100);
     let svg;
 
     beforeEach(() => {
         svg = select(document.body).append('svg');
-        appendAxes({
-            plot: svg,
+        appendAxes(svg, {
             xAxis,
             yAxis,
-            xLoc: {x: 10, y: 10},
-            yLoc: {x: 0, y: 100},
-            yLabelLoc: {x: 10, y: 10},
             yTitle: 'Label title'
         });
     });
@@ -37,19 +34,6 @@ describe('Chart axes', () => {
 
     it('axes appended', () => {
         // Should be translated
-        expect(svg.select('.x-axis').attr('transform')).toBe('translate(10, 10)');
-        expect(svg.select('.y-axis').attr('transform')).toBe('translate(0, 100)');
         expect(svg.select('.y-axis-label').text()).toEqual('Label title');
     });
-
-    it('tickValues should change with scale with new domain', () => {
-        const originalTickValues = yAxis.tickValues();
-        yScale.domain([2, 20]);
-        updateYAxis(yAxis, yScale);
-        expect(yAxis.tickValues()).not.toEqual(originalTickValues);
-
-        yScale.domain([0, 10]);
-        updateYAxis(yAxis, yScale);
-        expect(yAxis.tickValues()).toEqual(originalTickValues);
-    });
 });
diff --git a/assets/src/scripts/components/hydrograph/index.js b/assets/src/scripts/components/hydrograph/index.js
index d2533d531de5ac560e7a2213a6749df616bfbb77..45096c60cabed57de96d7241986688618c84c8ad 100644
--- a/assets/src/scripts/components/hydrograph/index.js
+++ b/assets/src/scripts/components/hydrograph/index.js
@@ -1,290 +1,176 @@
 /**
  * Hydrograph charting module.
  */
-const { bisector, extent, min, max } = require('d3-array');
+const { bisector } = require('d3-array');
+const { reduxConnect: connect, reduxDispatch: dispatch,
+        reduxFromState: fromState, reduxProvide: provide } = require('d3-redux');
 const { mouse, select } = require('d3-selection');
 const { line } = require('d3-shape');
-const { timeFormat } = require('d3-time-format');
 
 const { addSVGAccessibility, addSROnlyTable } = require('../../accessibility');
-const { getTimeseries, getPreviousYearTimeseries } = require('../../models');
 
-const { appendAxes, updateYAxis, createAxes } = require('./axes');
-const { createScales, createXScale, updateYScale } = require('./scales');
-
-
-// Define width, height and margin for the SVG.
-// Use a fixed size, and scale to device width using CSS.
-const WIDTH = 800;
-const HEIGHT = WIDTH / 2;
-const ASPECT_RATIO_PERCENT = `${100 * HEIGHT / WIDTH}%`;
-const MARGIN = {
-    top: 20,
-    right: 75,
-    bottom: 45,
-    left: 50
-};
+const { plotAxes } = require('./axes');
+const { WIDTH, HEIGHT, ASPECT_RATIO_PERCENT, MARGIN } = require('./layout');
+const { pointsSelector, validPointsSelector, isVisibleSelector } = require('./points');
+const { xScaleSelector, yScaleSelector } = require('./scales');
+const { Actions, configureStore } = require('./store');
 
 
 // Function that returns the left bounding point for a given chart point.
 const bisectDate = bisector(d => d.time).left;
 
 
-// Create a time formatting function from D3's timeFormat
-const formatTime = timeFormat('%c %Z');
-
-
-class Hydrograph {
-    /**
-     * @param {Array} data IV data as returned by models/getTimeseries
-     * @param {String} yLabel y-axis label
-     * @param {String} title for svg's title attribute
-     * @param {String} desc for svg's desc attribute
-     * @param {Node} element Dom node to insert
-     */
-    constructor({data=[], yLabel='Data', title='', desc='', element}) {
-        this._yLabel = yLabel;
-        this._title = title;
-        this._desc = desc;
-        this._element = element;
-        this._tsData = {};
-
-        if (data && data.length) {
-            this._tsData.current = data;
-            this._drawChart();
-        } else {
-            this._drawMessage('No data is available for this site.');
-        }
-    }
-
-    /**
-     * Add a new time series to the Hydrograph. The time series is assumed to be
-     * data that is over the same date range in a different year.
-     * @param {Array} data - IV data as returned by models.getTimeseires
-     */
-    addCompareTimeSeries(data) {
-        //Save data - will be needed in order to implement the tooltips
-        this._tsData.compare = data;
-
-        // Update the yScale by determining the new extent
-        const currentYExtent = extent(this._tsData.current, d => d.value);
-        const yExtent = extent(data, d => d.value);
-        const yDataExtent = [min([yExtent[0], currentYExtent[0]]), max([yExtent[1], currentYExtent[1]])];
-        updateYScale(this.scale.yScale, yDataExtent);
-
-        // Create a x scale for the new data
-        const xScale = createXScale(data, WIDTH - MARGIN.right);
-
-        // Update the yAxis
-        updateYAxis(this.axis.yAxis, this.scale.yScale);
-        this.svg.select('.y-axis')
-            .call(this.axis.yAxis);
-
-        //Update the current ts line
-        select('#ts-current')
-            .attr('d', this.currentLine(this._tsData.current));
-
-        // Add the new time series
-        this._plotDataLine(this.plot, {xScale: xScale, yScale: this.scale.yScale}, 'compare');
-    }
-
-    /**
-     * Remove the compare time series from the plot and rescale
-     */
-    removeCompareTimeSeries() {
-        // Remove the compare time series
-        this.svg.select('#ts-compare').remove();
-        this.svg.select('.x-top-axis').remove();
-        delete this._tsData.compare;
 
-        // Update the y scale and  redraw the axis
-        const currentYExtent = extent(this._tsData.current, d => d.value);
-        updateYScale(this.scale.yScale, currentYExtent);
-        updateYAxis(this.axis.yAxis, this.scale.yScale);
-        this.svg.select('.y-axis')
-            .call(this.axis.yAxis);
-
-        //Redraw the current ts
-        select('#ts-current')
-            .attr('d', this.currentLine(this._tsData.current));
-    }
-
-    _drawChart() {
-        // Set up parent element and SVG
-        this.svg = select(this._element)
-            .append('div')
-            .attr('class', 'hydrograph-container')
-            .style('padding-bottom', ASPECT_RATIO_PERCENT)
-            .append('svg')
-            .attr('preserveAspectRatio', 'xMinYMin meet')
-            .attr('viewBox', `0 0 ${WIDTH} ${HEIGHT}`);
-
-        addSVGAccessibility({
-            svg: this.svg,
-            title: this._title,
-            description: this._desc,
-            isInteractive: true
-        });
-
-        addSROnlyTable({
-            container: this._element,
-            columnNames: [this._title, 'Time'],
-            data: this._tsData.current.map((value) => {
-                return [value.value, value.time];
-            })
-        });
-        // We'll actually be appending to a <g> element
-        this.plot = this.svg.append('g')
-            .attr('transform', `translate(${MARGIN.left},${MARGIN.top})`);
-
-        // Create x/y scaling for the full (100%) view.
-        this.scale = createScales(
-            this._tsData.current,
-            WIDTH - MARGIN.right,
-            HEIGHT - (MARGIN.top + MARGIN.bottom)
-        );
-        this.axis = createAxes(this.scale.xScale, this.scale.yScale, -WIDTH + MARGIN.right);
-
-        // Draw the graph components with the given scaling.
-        appendAxes({
-            plot: this.plot,
-            xAxis: this.axis.xAxis,
-            yAxis: this.axis.yAxis,
-            xLoc: {x: 0, y: HEIGHT - (MARGIN.top + MARGIN.bottom)},
-            yLoc: {x: 0, y: 0},
-            yLabelLoc: {x: HEIGHT / -2 + MARGIN.top, y: -35},
-            yTitle: this._yLabel
-        });
-        this.currentLine = this._plotDataLine(this.plot, this.scale, 'current');
-        this._plotTooltips(this.plot, this.scale, 'current');
-    }
-
-    _drawMessage(message) {
-        // Set up parent element and SVG
-        this._element.innerHTML = '';
-        const alertBox = select(this._element)
-            .append('div')
+const drawMessage = function (elem, message) {
+    // Set up parent element and SVG
+    elem.innerHTML = '';
+    const alertBox = elem
+        .append('div')
             .attr('class', 'usa-alert usa-alert-warning')
             .append('div')
-            .attr('class', 'usa-alert-body');
-        alertBox.append('h3')
+                .attr('class', 'usa-alert-body');
+    alertBox
+        .append('h3')
             .attr('class', 'usa-alert-heading')
             .html('Hydrograph Alert');
-        alertBox.append('p')
+    alertBox
+        .append('p')
             .html(message);
-    }
-
-    _plotDataLine(plot, scale, tsDataKey) {
-        let tsLine = line()
-            .x(d => scale.xScale(d.time))
-            .y(d => scale.yScale(d.value));
+};
 
-        plot.append('path')
-            .datum(this._tsData[tsDataKey])
-            .classed('line', true)
-            .attr('id', 'ts-' + tsDataKey)
-            .attr('d', tsLine);
-        return tsLine;
-    }
 
-    _plotTooltips(plot, scale, tsDataKey) {
-        // Create a node to hightlight the currently selected date/time.
-        let focus = plot.append('g')
-            .attr('class', 'focus')
-            .style('display', 'none');
+const plotDataLine = function (elem, store, tsDataKey) {
+    const elemId = 'ts-' + tsDataKey;
+    elem.selectAll(`#${elemId}`).remove();
 
-        focus.append('circle')
-            .attr('r', 7.5);
+    const state = store.getState();
+    if (!isVisibleSelector(state, tsDataKey)) {
+        return;
+    }
 
-        focus.append('text');
+    elem.datum(fromState(state => validPointsSelector(state, tsDataKey)))
+        .append('path')
+            .classed('line', true)
+            .attr('id', elemId)
+            .attr('d', line().x(d => d.x)
+                             .y(d => d.y));
+};
 
-        plot.append('rect')
-            .attr('class', 'overlay')
-            .attr('width', WIDTH)
-            .attr('height', HEIGHT)
-            .on('mouseover', () => focus.style('display', null))
-            .on('mouseout', () => focus.style('display', 'none'))
-            .on('mousemove', (d, i, nodes) => {
-                // Get the nearest data point for the current mouse position.
-                let time = scale.xScale.invert(mouse(nodes[i])[0]);
-                let {datum, index} = this._getNearestTime(time, tsDataKey);
 
-                // Move the focus node to this date/time.
-                focus.attr('transform', `translate(${scale.xScale(datum.time)}, ${scale.yScale(datum.value)})`);
+const getNearestTime = function (data, time) {
+    let index = bisectDate(data, time, 1);
+    let datum;
+    let d0 = data[index - 1];
+    let d1 = data[index];
 
-                // Draw text, anchored to the left or right, depending on
-                // which side of the graph the point is on.
-                let isFirstHalf = index < this._tsData[tsDataKey].length / 2;
-                focus.select('text')
-                    .text(() => datum.label)
-                    .attr('text-anchor', isFirstHalf ? 'start' : 'end')
-                    .attr('x', isFirstHalf ? 15 : -15)
-                    .attr('dy', isFirstHalf ? '.31em' : '-.31em');
-            });
+    if (d0 && d1) {
+        datum = time - d0.time > d1.time - time ? d1 : d0;
+    } else {
+        datum = d0 || d1;
     }
 
-    _getNearestTime(time, tsDataKey) {
-        let index = bisectDate(this._tsData[tsDataKey], time, 1);
+    // Return the nearest data point and its index.
+    return {
+        datum,
+        index: datum === d0 ? index - 1 : index
+    };
+};
 
-        let datum;
-        let d0 = this._tsData[tsDataKey][index - 1];
-        let d1 = this._tsData[tsDataKey][index];
-        if (d0 && d1) {
-            datum = time - d0.time > d1.time - time ? d1 : d0;
-        } else {
-            datum = d0 || d1;
-        }
 
-        // Return the nearest data point and its index.
-        return {
-            datum,
-            index: datum === d0 ? index - 1 : index
-        };
-    }
-}
+const plotTooltips = function (elem, store, tsDataKey) {
+    // Create a node to hightlight the currently selected date/time.
+    let focus = elem.append('g')
+        .attr('class', 'focus')
+        .style('display', 'none');
+    focus.append('circle')
+        .attr('r', 7.5);
+    focus.append('text');
+
+    elem.append('rect')
+        .attr('class', 'overlay')
+        .attr('width', WIDTH)
+        .attr('height', HEIGHT)
+        .on('mouseover', () => focus.style('display', null))
+        .on('mouseout', () => focus.style('display', 'none'))
+        .on('mousemove', function () {
+            const state = store.getState();
+            const xScale = xScaleSelector(state, tsDataKey);
+            const yScale = yScaleSelector(state, tsDataKey);
+            const data = pointsSelector(state, tsDataKey);
+
+            // Get the nearest data point for the current mouse position.
+            const time = xScale.invert(mouse(this)[0]);
+            const {datum, index} = getNearestTime(data, time);
+            if (!datum) {
+                return;
+            }
 
+            // Move the focus node to this date/time.
+            focus.attr('transform', `translate(${xScale(datum.time)}, ${yScale(datum.value)})`);
+
+            // Draw text, anchored to the left or right, depending on
+            // which side of the graph the point is on.
+            const isFirstHalf = index < data.length / 2;
+            focus.select('text')
+                .text(() => datum.label)
+                .attr('text-anchor', isFirstHalf ? 'start' : 'end')
+                .attr('x', isFirstHalf ? 15 : -15)
+                .attr('dy', isFirstHalf ? '.31em' : '-.31em');
+        });
+};
 
-function attachToNode(node, {siteno}) {
-    let hydrograph;
-    let getLastYearTS;
-    getTimeseries({sites: [siteno]}).then((series) => {
-            let dataIsValid = series && series[0] &&
-                !series[0].values.some(d => d.value === -999999);
-            hydrograph = new Hydrograph({
-                element: node,
-                data: dataIsValid ? series[0].values : [],
-                yLabel: dataIsValid ? series[0].variableDescription : 'No data',
-                title: dataIsValid ? series[0].variableName : '',
-                desc: dataIsValid ? series[0].variableDescription + ' from ' +
-                    formatTime(series[0].seriesStartDate) + ' to ' +
-                    formatTime(series[0].seriesEndDate) : ''
-            });
-            if (dataIsValid) {
-                getLastYearTS = getPreviousYearTimeseries({
-                    site: node.dataset.siteno,
-                    startTime: series[0].seriesStartDate,
-                    endTime: series[0].seriesEndDate
+
+const timeSeriesGraph = function (elem, store) {
+    elem.append('div')
+        .attr('class', 'hydrograph-container')
+        .style('padding-bottom', ASPECT_RATIO_PERCENT)
+        .append('svg')
+            .attr('preserveAspectRatio', 'xMinYMin meet')
+            .attr('viewBox', `0 0 ${WIDTH} ${HEIGHT}`)
+            .call(connect(function (elem) {
+                let state = store.getState();
+                elem.call(addSVGAccessibility, {
+                    title: state.title,
+                    description: state.desc,
+                    isInteractive: true
                 });
-            }
-        }, () =>
-            hydrograph = new Hydrograph({
-                element: node,
-                data: []
+            }))
+            .append('g')
+                .attr('transform', `translate(${MARGIN.left},${MARGIN.top})`)
+                .call(connect(plotAxes), store)
+                .call(connect(plotDataLine), store, 'current')
+                .call(connect(plotDataLine), store, 'compare')
+                //.call(plotTooltips, store, 'compare')
+                .call(plotTooltips, store, 'current');
+    elem.call(connect(function (elem) {
+        const state = store.getState();
+        elem.call(addSROnlyTable, {
+            columnNames: [state.title, 'Time'],
+            data: pointsSelector(state, 'current').map((value) => {
+                return [value.value, value.time];
             })
-    );
-    let lastYearInput = node.getElementsByClassName('hydrograph-last-year-input');
-    if (lastYearInput.length > 0) {
-        lastYearInput[0].addEventListener('change', (evt) => {
-            if (evt.target.checked) {
-                getLastYearTS.then((series) => {
-                    hydrograph.addCompareTimeSeries(series[0].values);
-                });
-            } else {
-                hydrograph.removeCompareTimeSeries();
-            }
         });
+    }));
+};
+
+
+const attachToNode = function (node, {siteno} = {}) {
+    if (!siteno) {
+        select(node).call(drawMessage, 'No data is available.');
+        return;
     }
-}
+
+    let store = configureStore();
+
+    select(node)
+        .call(provide(store))
+        .call(timeSeriesGraph, store)
+        .select('.hydrograph-last-year-input')
+            .on('change', dispatch(function () {
+                return Actions.toggleTimeseries('compare', this.checked);
+            }));
+    store.dispatch(Actions.retrieveTimeseries(siteno));
+};
 
 
-module.exports = {Hydrograph, attachToNode};
+module.exports = {attachToNode, getNearestTime, timeSeriesGraph};
diff --git a/assets/src/scripts/components/hydrograph/index.spec.js b/assets/src/scripts/components/hydrograph/index.spec.js
index 065e529f3c23ad99b26a95059d9c3f346c612073..cf2645bb3daf264fd2cf6a21ceafe64c035a394a 100644
--- a/assets/src/scripts/components/hydrograph/index.spec.js
+++ b/assets/src/scripts/components/hydrograph/index.spec.js
@@ -1,6 +1,8 @@
 const { select, selectAll } = require('d3-selection');
+const { reduxProvide: provide } = require('d3-redux');
 
-const Hydrograph = require('./index').Hydrograph;
+const { attachToNode, getNearestTime, timeSeriesGraph } = require('./index');
+const { Actions, configureStore } = require('./store');
 
 
 describe('Hydrograph charting module', () => {
@@ -18,35 +20,59 @@ describe('Hydrograph charting module', () => {
     });
 
     it('empty graph displays warning', () => {
-        new Hydrograph({element: graphNode});
+        attachToNode(graphNode, {});
         expect(graphNode.innerHTML).toContain('No data is available');
     });
 
     it('single data point renders', () => {
-        new Hydrograph({
-            element: graphNode,
-            data: [{
-                time: new Date(),
-                value: 10,
-                label: 'Label'
-            }]
+        const store = configureStore({
+            tsData: {
+                current: {
+                    data: [{
+                        time: new Date(),
+                        value: 10,
+                        label: 'Label'
+                    }],
+                    show: true
+                },
+                compare: {
+                    data: [],
+                    show: false
+                }
+            },
+            title: '',
+            desc: ''
         });
+        select(graphNode)
+            .call(provide(store))
+            .call(timeSeriesGraph, store);
         expect(graphNode.innerHTML).toContain('hydrograph-container');
     });
 
     describe('SVG has been made accessibile', () => {
         let svg;
         beforeEach(() => {
-            new Hydrograph({
-                element: graphNode,
+            const store = configureStore({
+                tsData: {
+                    current: {
+                        data: [{
+                            time: new Date(),
+                            value: 10,
+                            label: 'Label'
+                        }],
+                        show: true
+                    },
+                    compare: {
+                        data: [],
+                        show: false
+                    }
+                },
                 title: 'My Title',
                 desc: 'My Description',
-                data: [{
-                    time: new Date(),
-                    value: 10,
-                    label: 'Label'
-                }]
             });
+            select(graphNode)
+                .call(provide(store))
+                .call(timeSeriesGraph, store);
             svg = select('svg');
         });
 
@@ -66,7 +92,23 @@ describe('Hydrograph charting module', () => {
     describe('Renders real data from site #05370000', () => {
         /* eslint no-use-before-define: "ignore" */
         beforeEach(() => {
-            new Hydrograph({element: graphNode, data: MOCK_DATA});
+            const store = configureStore({
+                tsData: {
+                    current: {
+                        data: MOCK_DATA,
+                        show: true
+                    },
+                    compare: {
+                        data: [],
+                        show: false
+                    }
+                },
+                title: 'My Title',
+                desc: 'My Description',
+            });
+            select(graphNode)
+                .call(provide(store))
+                .call(timeSeriesGraph, store);
         });
 
         it('should render an svg node', () => {
@@ -84,7 +126,6 @@ describe('Hydrograph charting module', () => {
     });
 
     describe('Hydrograph tooltips', () => {
-        let graph;
         let data = [12, 13, 14, 15, 16].map(hour => {
             return {
                 time: new Date(`2018-01-03T${hour}:00:00.000Z`),
@@ -92,9 +133,6 @@ describe('Hydrograph charting module', () => {
                 value: 0
             };
         });
-        beforeEach(() => {
-            graph = new Hydrograph({element: graphNode, data: data});
-        });
 
         it('return correct data points via getNearestTime' , () => {
             // Check each date with the given offset against the hourly-spaced
@@ -108,7 +146,8 @@ describe('Hydrograph charting module', () => {
                         expected = {datum: data[index + 1], index: index + 1};
                     }
                     let time = new Date(datum.time.getTime() + offset);
-                    let returned = graph._getNearestTime(time, 'current');
+                    let returned = getNearestTime(data, time);
+
                     expect(returned.datum.time).toBe(expected.datum.time);
                     expect(returned.datum.index).toBe(expected.datum.index);
                 }
@@ -129,17 +168,33 @@ describe('Hydrograph charting module', () => {
     describe('Adding and removing compare time series', () => {
         /* eslint no-use-before-define: "ignore" */
         let hydrograph;
+        let store;
         beforeEach(() => {
-            hydrograph = new Hydrograph({element: graphNode, data: MOCK_DATA});
-            hydrograph.addCompareTimeSeries(MOCK_DATA_FOR_PREVIOUS_YEAR);
+            store = configureStore({
+                tsData: {
+                    current: {
+                        data: MOCK_DATA,
+                        show: true
+                    },
+                    compare: {
+                        data: MOCK_DATA_FOR_PREVIOUS_YEAR,
+                        show: true
+                    }
+                },
+                title: 'My Title',
+                desc: 'My Description',
+            });
+            select(graphNode)
+                .call(provide(store))
+                .call(timeSeriesGraph, store);
         });
 
         it('Should render two lines', () => {
             expect(selectAll('svg path.line').size()).toBe(2);
         });
 
-        it('Should remove one of lthe lines when removing the compare time series', () => {
-            hydrograph.removeCompareTimeSeries();
+        it('Should remove one of the lines when removing the compare time series', () => {
+            store.dispatch(Actions.toggleTimeseries('compare', false));
             expect(selectAll('svg path.line').size()).toBe(1);
         });
 
diff --git a/assets/src/scripts/components/hydrograph/layout.js b/assets/src/scripts/components/hydrograph/layout.js
new file mode 100644
index 0000000000000000000000000000000000000000..8cf238c1a5757d42ba26dd8113937f4f16aae535
--- /dev/null
+++ b/assets/src/scripts/components/hydrograph/layout.js
@@ -0,0 +1,11 @@
+// Define width, height and margin for the SVG.
+// Use a fixed size, and scale to device width using CSS.
+export const WIDTH = 800;
+export const HEIGHT = WIDTH / 2;
+export const ASPECT_RATIO_PERCENT = `${100 * HEIGHT / WIDTH}%`;
+export const MARGIN = {
+    top: 20,
+    right: 75,
+    bottom: 45,
+    left: 50
+};
diff --git a/assets/src/scripts/components/hydrograph/points.js b/assets/src/scripts/components/hydrograph/points.js
new file mode 100644
index 0000000000000000000000000000000000000000..5b08913deec224d36098e02e76900cb81331a02b
--- /dev/null
+++ b/assets/src/scripts/components/hydrograph/points.js
@@ -0,0 +1,40 @@
+const { createSelector } = require('reselect');
+
+const { xScaleSelector, yScaleSelector } = require('./scales');
+
+
+const pointsSelector = function (state, tsDataKey) {
+    if (state.tsData[tsDataKey]) {
+        return state.tsData[tsDataKey].data;
+    }
+    return [];
+};
+
+
+const isVisibleSelector = function (state, tsDataKey) {
+    if (state.tsData[tsDataKey]) {
+        return state.tsData[tsDataKey].show;
+    }
+    return false;
+};
+
+
+const validPointsSelector = createSelector(
+    pointsSelector,
+    xScaleSelector,
+    yScaleSelector,
+    (tsData, xScale, yScale) => {
+        let a = tsData
+            .filter(d => d.value !== undefined)
+            .map(d => {
+                return {
+                    x: xScale(d.time),
+                    y: yScale(d.value)
+                };
+            });
+        return a;
+    }
+);
+
+
+module.exports = { pointsSelector, validPointsSelector, isVisibleSelector };
diff --git a/assets/src/scripts/components/hydrograph/scales.js b/assets/src/scripts/components/hydrograph/scales.js
index a927f1f02856a8e480483096b697794dee526396..aff9145e18a866278c09e9b0640bf85e5c648fbd 100644
--- a/assets/src/scripts/components/hydrograph/scales.js
+++ b/assets/src/scripts/components/hydrograph/scales.js
@@ -1,8 +1,12 @@
 const { extent } = require('d3-array');
 const { scaleLinear, scaleTime } = require('d3-scale');
+const { createSelector } = require('reselect');
+
+const { WIDTH, HEIGHT, MARGIN } = require('./layout');
 
 const paddingRatio = 0.2;
 
+
 /**
  *  Return domainExtent padded on both ends by paddingRatio
  *  @param {Array} domainExtent - array of two numbers
@@ -14,14 +18,14 @@ function extendDomain(domainExtent) {
 }
 
 /**
- * Create an xcale oriented on the left
+ * Create an x-scale oriented on the left
  * @param {Array} data - Array contains {time, ...}
  * @param {Number} xSize - range of scale
  * @eturn {Object} d3 scale for time.
  */
-function createXScale(data, xSize) {
-    // Calculate max and min for data
-    const xExtent = extent(data, d => d.time);
+function createXScale(values, xSize) {
+    // Calculate max and min for values
+    const xExtent = values.length ? extent(values, d => d.time) : [0, 1];
 
     // xScale is oriented on the left
     return scaleTime()
@@ -30,19 +34,42 @@ function createXScale(data, xSize) {
 }
 
 /**
- * Create an ycale oriented on the bottom
+ * Create an yscale oriented on the bottom
  * @param {Array} data - Array contains {value, ...}
  * @param {Number} xSize - range of scale
  * @eturn {Object} d3 scale for value.
  */
-function createYScale(data, ySize) {
+function createYScale(tsData, ySize) {
+    let yExtent;
+
     // Calculate max and min for data
-    const yExtent = extent(data, d => d.value);
+    for (let key of Object.keys(tsData)) {
+        if (!tsData[key].show || tsData[key].data.length === 0) {
+            continue;
+        }
+
+        const thisExtent = extent(tsData[key].data, d => d.value);
+        if (yExtent !== undefined) {
+            yExtent = [
+                Math.min(thisExtent[0], yExtent[0]),
+                Math.max(thisExtent[1], yExtent[1])
+            ];
+        } else {
+            yExtent = thisExtent;
+        }
+    }
+
+    // Add padding to the extent and handle empty data sets.
+    if (yExtent) {
+        yExtent = extendDomain(yExtent);
+    } else {
+        yExtent = [0, 1];
+    }
 
     // yScale is oriented on the bottom
     return scaleLinear()
         .range([ySize, 0])
-        .domain(extendDomain(yExtent));
+        .domain(yExtent);
 }
 
 /**
@@ -52,22 +79,31 @@ function createYScale(data, ySize) {
  * @param  {Number} ySize Y range of scale
  * @return {Object}        {xScale, yScale}
  */
-function createScales(data, xSize, ySize) {
-    const xScale = createXScale(data, xSize);
-    const yScale = createYScale(data, ySize);
+function createScales(tsData, xSize, ySize) {
+    const xScale = createXScale(tsData.current.data, xSize);
+    const yScale = createYScale(tsData, ySize);
 
     return {xScale, yScale};
 }
 
-/**
- * Updates the domain of yScale with the new extent including the paddingRatio
- * @param yScale
- * @param newYDataExtent
- */
-function updateYScale(yScale, newYDataExtent) {
-    yScale.domain(extendDomain(newYDataExtent));
 
-}
+const xScaleSelector = createSelector(
+    (state) => state.tsData,
+    (state, tsDataKey = 'current') => tsDataKey,
+    (tsData, tsDataKey) => {
+        if (tsData[tsDataKey]) {
+            return createXScale(tsData[tsDataKey].data, WIDTH - MARGIN.right);
+        } else {
+            return null;
+        }
+    }
+);
+
+
+const yScaleSelector = createSelector(
+    (state) => state.tsData,
+    (tsData) => createYScale(tsData, HEIGHT - (MARGIN.top + MARGIN.bottom))
+);
 
 
-module.exports = {createScales, createXScale, createYScale, updateYScale};
+module.exports = {createScales, xScaleSelector, yScaleSelector};
diff --git a/assets/src/scripts/components/hydrograph/scales.spec.js b/assets/src/scripts/components/hydrograph/scales.spec.js
index 6ea3acd6a56ef8dc3e9694e760707975afea6c4e..720895561964df30628a857fe892a29240f819ec 100644
--- a/assets/src/scripts/components/hydrograph/scales.spec.js
+++ b/assets/src/scripts/components/hydrograph/scales.spec.js
@@ -1,4 +1,4 @@
-const { createScales, updateYScale } = require('./scales');
+const { createScales } = require('./scales');
 
 
 describe('Charting scales', () => {
@@ -9,10 +9,11 @@ describe('Charting scales', () => {
             value: hour
         };
     });
-    let {xScale, yScale} = createScales(data, 200, 100);
+    let tsData = {current: {data: data}};
+    let {xScale, yScale} = createScales(tsData, 200, 100);
 
     it('scales created', () => {
-        let {xScale, yScale} = createScales(data, 200, 100);
+        let {xScale, yScale} = createScales(tsData, 200, 100);
         expect(xScale).toEqual(jasmine.any(Function));
         expect(yScale).toEqual(jasmine.any(Function));
     });
@@ -40,20 +41,4 @@ describe('Charting scales', () => {
         expect(yScale(.5)).not.toBeNaN();
         expect(yScale(.999)).not.toBeNaN();
     });
-
-    it('Domain should be extended if the new data extent exceeds the extent used to create the yScale', () => {
-        const currentDomain = yScale.domain();
-        updateYScale(yScale, [-1, 25]);
-        const newDomain = yScale.domain();
-        expect(newDomain[1]).toBeGreaterThan(currentDomain[1]);
-        expect(newDomain[0]).toBeLessThan(currentDomain[0]);
-    });
-
-    it('Domain should not be descreased if the new data extent is less than the extents used to create the yScale', () => {
-        const currentDomain = yScale.domain();
-        updateYScale(yScale, [2, 20]);
-        const newDomain = yScale.domain();
-        expect(newDomain[0]).toBeGreaterThan(currentDomain[0]);
-        expect(newDomain[1]).toBeLessThan(currentDomain[1]);
-    });
 });
diff --git a/assets/src/scripts/components/hydrograph/store.js b/assets/src/scripts/components/hydrograph/store.js
new file mode 100644
index 0000000000000000000000000000000000000000..8f663dfba0f8107f10cd122ef07143e1ae440b35
--- /dev/null
+++ b/assets/src/scripts/components/hydrograph/store.js
@@ -0,0 +1,137 @@
+const { timeFormat } = require('d3-time-format');
+const { applyMiddleware, createStore } = require('redux');
+const { default: thunk } = require('redux-thunk');
+
+const { getPreviousYearTimeseries, getTimeseries } = require('../../models');
+
+// Create a time formatting function from D3's timeFormat
+const formatTime = timeFormat('%c %Z');
+
+
+export const Actions = {
+    retrieveTimeseries(siteno, startDate=null, endDate=null) {
+        return function (dispatch) {
+            return getTimeseries({sites: [siteno], startDate, endDate}).then(
+                series => {
+                    dispatch(Actions.addTimeseries('current', siteno, series[0]));
+
+                    // Trigger a call to get last year's data
+                    const startTime = series[0].seriesStartDate;
+                    const endTime = series[0].seriesEndDate;
+                    dispatch(Actions.retrieveCompareTimeseries(siteno, startTime, endTime));
+                },
+                () => dispatch(Actions.resetTimeseries('current'))
+            );
+        };
+    },
+    retrieveCompareTimeseries(site, startTime, endTime) {
+        return function (dispatch) {
+            return getPreviousYearTimeseries({site, startTime, endTime}).then(
+                series => dispatch(Actions.addTimeseries('compare', site, series[0], false)),
+                () => dispatch(Actions.resetTimeseries('compare'))
+            );
+        };
+    },
+    toggleTimeseries(key, show) {
+        return {
+            type: 'TOGGLE_TIMESERIES',
+            key,
+            show
+        };
+    },
+    addTimeseries(key, siteno, data, show=true) {
+        return {
+            type: 'ADD_TIMESERIES',
+            key,
+            siteno,
+            data,
+            show
+        };
+    },
+    resetTimeseries(key) {
+        return {
+            type: 'RESET_TIMESERIES',
+            key
+        };
+    }
+};
+
+
+const timeSeriesReducer = function (state={}, action) {
+    switch (action.type) {
+        case 'ADD_TIMESERIES':
+            // If data is valid
+            if (action.data && action.data.values &&
+                    !action.data.values.some(d => d.value === -999999)) {
+                return {
+                    ...state,
+                    tsData: {
+                        ...state.tsData,
+                        [action.key]: {
+                            ...state.tsData[action.key],
+                            data: action.data.values,
+                            show: action.show
+                        }
+                    },
+                    title: action.data.variableName,
+                    desc: action.data.variableDescription + ' from ' +
+                        formatTime(action.data.seriesStartDate) + ' to ' +
+                        formatTime(action.data.seriesEndDate)
+                };
+            } else {
+                return state.dispatch(Actions.resetTimeseries());
+            }
+
+        case 'TOGGLE_TIMESERIES':
+            return {
+                ...state,
+                tsData: {
+                    ...state.tsData,
+                    [action.key]: {
+                        ...state.tsData[action.key],
+                        show: action.show
+                    }
+                }
+            };
+
+        case 'RESET_TIMESERIES':
+            return {
+                ...state,
+                tsData: {
+                    ...state.tsData,
+                    [action.key]: {
+                        ...state.tsData[action.key],
+                        data: [],
+                        show: false
+                    }
+                }
+            };
+
+        default:
+            return state;
+    }
+};
+
+
+export const configureStore = function (initialState) {
+    initialState = {
+        tsData: {
+            current: {
+                data: [],
+                show: true
+            },
+            compare: {
+                data: [],
+                show: false
+            }
+        },
+        title: '',
+        desc: '',
+        ...initialState
+    };
+    return createStore(
+        timeSeriesReducer,
+        initialState,
+        applyMiddleware(thunk)
+    );
+};
diff --git a/package-lock.json b/package-lock.json
index ffb3afabc95e826b54598760a32fa3def9ffeca4..c22f670582f72dd5fd7f5a403b6ab46d3da57c45 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -656,6 +656,12 @@
       "integrity": "sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4=",
       "dev": true
     },
+    "babel-plugin-syntax-object-rest-spread": {
+      "version": "6.13.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz",
+      "integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=",
+      "dev": true
+    },
     "babel-plugin-syntax-trailing-function-commas": {
       "version": "6.22.0",
       "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz",
@@ -918,6 +924,16 @@
         "babel-runtime": "6.26.0"
       }
     },
+    "babel-plugin-transform-object-rest-spread": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz",
+      "integrity": "sha1-DzZpLVD+9rfi1LOsFHgTepY7ewY=",
+      "dev": true,
+      "requires": {
+        "babel-plugin-syntax-object-rest-spread": "6.13.0",
+        "babel-runtime": "6.26.0"
+      }
+    },
     "babel-plugin-transform-regenerator": {
       "version": "6.26.0",
       "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz",
@@ -2723,6 +2739,14 @@
       "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-1.1.0.tgz",
       "integrity": "sha1-ZkLlBsb6OmSFldKyRpeIqNElKdM="
     },
+    "d3-redux": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/d3-redux/-/d3-redux-0.0.6.tgz",
+      "integrity": "sha512-vU5oQQKZUD/9pmhQ/GoECbbU6tFHzoKQaskuLJtX3ORDLXYJzrf2iFo+FJKf43FQIK/rE9RyogKYlHXngpq0CQ==",
+      "requires": {
+        "d3-selection": "1.2.0"
+      }
+    },
     "d3-request": {
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/d3-request/-/d3-request-1.0.6.tgz",
@@ -6353,8 +6377,7 @@
     "js-tokens": {
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
-      "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=",
-      "dev": true
+      "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls="
     },
     "js-yaml": {
       "version": "3.10.0",
@@ -6879,8 +6902,12 @@
     "lodash": {
       "version": "4.17.4",
       "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
-      "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=",
-      "dev": true
+      "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4="
+    },
+    "lodash-es": {
+      "version": "4.17.4",
+      "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.4.tgz",
+      "integrity": "sha1-3MHXVS4VCgZABzupyzHXDwMpUOc="
     },
     "lodash.assign": {
       "version": "4.2.0",
@@ -6899,6 +6926,12 @@
       "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
       "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168="
     },
+    "lodash.isplainobject": {
+      "version": "4.0.6",
+      "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+      "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=",
+      "dev": true
+    },
     "lodash.memoize": {
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz",
@@ -7021,7 +7054,6 @@
       "version": "1.3.1",
       "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz",
       "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=",
-      "dev": true,
       "requires": {
         "js-tokens": "3.0.2"
       }
@@ -9135,6 +9167,31 @@
       "dev": true,
       "optional": true
     },
+    "redux": {
+      "version": "3.7.2",
+      "resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz",
+      "integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==",
+      "requires": {
+        "lodash": "4.17.4",
+        "lodash-es": "4.17.4",
+        "loose-envify": "1.3.1",
+        "symbol-observable": "1.2.0"
+      }
+    },
+    "redux-mock-store": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/redux-mock-store/-/redux-mock-store-1.5.1.tgz",
+      "integrity": "sha512-B+iZ98ESHw4EAWVLKUknQlop1OdLKOayGRmd6KavNtC0zoSsycD8hTt0hEr1eUTw2gmYJOdfBY5QAgZweTUcLQ==",
+      "dev": true,
+      "requires": {
+        "lodash.isplainobject": "4.0.6"
+      }
+    },
+    "redux-thunk": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.2.0.tgz",
+      "integrity": "sha1-5hWhbha0ehmlFXZhM9Hj6Zt4UuU="
+    },
     "regenerate": {
       "version": "1.3.3",
       "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.3.tgz",
@@ -9332,6 +9389,11 @@
       "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
       "dev": true
     },
+    "reselect": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/reselect/-/reselect-3.0.1.tgz",
+      "integrity": "sha1-79qpjqdFEyTQkrKyFjpqHXqaIUc="
+    },
     "resolve": {
       "version": "1.5.0",
       "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz",
@@ -10399,6 +10461,11 @@
       "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
       "dev": true
     },
+    "symbol-observable": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
+      "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ=="
+    },
     "syntax-error": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.3.0.tgz",
diff --git a/package.json b/package.json
index 95f015501a9ceb64f7c97b28d555500ec7a4922f..cbf57f9e5e3e71a3cf9b77f8728f6adfedd89ea7 100644
--- a/package.json
+++ b/package.json
@@ -38,6 +38,7 @@
   "homepage": "https://github.com/usgs/waterdataui#readme",
   "devDependencies": {
     "babel-core": "^6.26.0",
+    "babel-plugin-transform-object-rest-spread": "^6.26.0",
     "babel-polyfill": "^6.26.0",
     "babel-preset-env": "^1.6.1",
     "babelify": "^8.0.0",
@@ -57,6 +58,7 @@
     "nodemon": "^1.14.11",
     "npm-run-all": "^4.1.2",
     "proxyquireify": "^3.2.1",
+    "redux-mock-store": "^1.5.1",
     "tinyify": "^2.4.0",
     "uglify-js": "^3.3.5",
     "uglifycss": "0.0.27",
@@ -64,9 +66,13 @@
   },
   "dependencies": {
     "d3": "^4.12.2",
+    "d3-redux": "0.0.6",
     "esri-leaflet": "^2.1.2",
     "font-awesome": "^4.7.0",
     "leaflet": "^1.3.1",
+    "redux": "^3.7.2",
+    "redux-thunk": "^2.2.0",
+    "reselect": "^3.0.1",
     "uswds": "^1.4.4"
   },
   "browserify": {