diff --git a/src/traces/table/attributes.js b/src/traces/table/attributes.js
index 947194ba357..4cb37afe919 100644
--- a/src/traces/table/attributes.js
+++ b/src/traces/table/attributes.js
@@ -48,7 +48,10 @@ module.exports = overrideAll({
         arrayOk: true,
         dflt: null,
         role: 'style',
-        description: 'The width of cells.'
+        description: [
+            'The width of columns expressed as a ratio. Columns fill the available width',
+            'in proportion of their specified column widths.'
+        ].join(' ')
     },
 
     columnorder: {
diff --git a/src/traces/table/data_preparation_helper.js b/src/traces/table/data_preparation_helper.js
index 62e07a248cd..5988cdd29e0 100644
--- a/src/traces/table/data_preparation_helper.js
+++ b/src/traces/table/data_preparation_helper.js
@@ -16,11 +16,12 @@ module.exports = function calc(gd, trace) {
     var headerValues = trace.header.values.map(function(c) {
         return Array.isArray(c) ? c : [c];
     });
+    var cellsValues = trace.cells.values;
     var domain = trace.domain;
     var groupWidth = Math.floor(gd._fullLayout._size.w * (domain.x[1] - domain.x[0]));
     var groupHeight = Math.floor(gd._fullLayout._size.h * (domain.y[1] - domain.y[0]));
-    var headerRowHeights = headerValues[0].map(function() {return trace.header.height;});
-    var rowHeights = trace.cells.values[0].map(function() {return trace.cells.height;});
+    var headerRowHeights = headerValues.length ? headerValues[0].map(function() {return trace.header.height;}) : [];
+    var rowHeights = cellsValues.length ? cellsValues[0].map(function() {return trace.cells.height;}) : [];
     var headerHeight = headerRowHeights.reduce(function(a, b) {return a + b;}, 0);
     var scrollHeight = groupHeight - headerHeight;
     var minimumFillHeight = scrollHeight + c.uplift;
diff --git a/src/traces/table/defaults.js b/src/traces/table/defaults.js
index 161a8742802..a30a08f9e47 100644
--- a/src/traces/table/defaults.js
+++ b/src/traces/table/defaults.js
@@ -11,9 +11,9 @@
 var Lib = require('../../lib');
 var attributes = require('./attributes');
 
-function defaultColumnOrder(traceIn, coerce) {
-    var specifiedColumnOrder = traceIn.columnorder || [];
-    var commonLength = traceIn.header.values.length;
+function defaultColumnOrder(traceOut, coerce) {
+    var specifiedColumnOrder = traceOut.columnorder || [];
+    var commonLength = traceOut.header.values.length;
     var truncated = specifiedColumnOrder.slice(0, commonLength);
     var sorted = truncated.slice().sort(function(a, b) {return a - b;});
     var oneStepped = truncated.map(function(d) {return sorted.indexOf(d);});
@@ -28,28 +28,10 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
         return Lib.coerce(traceIn, traceOut, attributes, attr, dflt);
     }
 
-    var fontDflt = {
-        family: layout.font.family,
-        size: layout.font.size,
-        color: layout.font.color
-    };
-
     coerce('domain.x');
     coerce('domain.y');
 
     coerce('columnwidth');
-    defaultColumnOrder(traceIn, coerce);
-
-    coerce('cells.values');
-    coerce('cells.format');
-    coerce('cells.align');
-    coerce('cells.prefix');
-    coerce('cells.suffix');
-    coerce('cells.height');
-    coerce('cells.line.width');
-    coerce('cells.line.color');
-    coerce('cells.fill.color');
-    Lib.coerceFont(coerce, 'cells.font', fontDflt);
 
     coerce('header.values');
     coerce('header.format');
@@ -61,5 +43,18 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
     coerce('header.line.width');
     coerce('header.line.color');
     coerce('header.fill.color');
-    Lib.coerceFont(coerce, 'header.font', fontDflt);
+    Lib.coerceFont(coerce, 'header.font', Lib.extendFlat({}, layout.font));
+
+    defaultColumnOrder(traceOut, coerce);
+
+    coerce('cells.values');
+    coerce('cells.format');
+    coerce('cells.align');
+    coerce('cells.prefix');
+    coerce('cells.suffix');
+    coerce('cells.height');
+    coerce('cells.line.width');
+    coerce('cells.line.color');
+    coerce('cells.fill.color');
+    Lib.coerceFont(coerce, 'cells.font', Lib.extendFlat({}, layout.font));
 };
diff --git a/src/traces/table/index.js b/src/traces/table/index.js
index ab5d7080fc1..3eec6ed12e1 100644
--- a/src/traces/table/index.js
+++ b/src/traces/table/index.js
@@ -18,7 +18,7 @@ Table.plot = require('./plot');
 Table.moduleType = 'trace';
 Table.name = 'table';
 Table.basePlotModule = require('./base_plot');
-Table.categories = [];
+Table.categories = ['noOpacity'];
 Table.meta = {
     description: [
         'Table view for detailed data viewing.',
diff --git a/src/traces/table/plot.js b/src/traces/table/plot.js
index efde0afa16f..e3481104215 100644
--- a/src/traces/table/plot.js
+++ b/src/traces/table/plot.js
@@ -93,6 +93,8 @@ module.exports = function plot(gd, wrappedTraceHolders) {
         .append('g')
         .classed(c.cn.yColumn, true);
 
+    yColumn.exit().remove();
+
     yColumn
         .attr('transform', function(d) {return 'translate(' + d.x + ' 0)';})
         .call(d3.behavior.drag()
@@ -242,7 +244,7 @@ function renderScrollbarKit(tableControlView, gd, bypassVisibleBar) {
 
     function calcTotalHeight(d) {
         var blocks = d.rowBlocks;
-        return firstRowAnchor(blocks, blocks.length - 1) + rowsHeight(blocks[blocks.length - 1], Infinity);
+        return firstRowAnchor(blocks, blocks.length - 1) + (blocks.length ? rowsHeight(blocks[blocks.length - 1], Infinity) : 1);
     }
 
     var scrollbarKit = tableControlView.selectAll('.' + c.cn.scrollbarKit)
@@ -288,7 +290,7 @@ function renderScrollbarKit(tableControlView, gd, bypassVisibleBar) {
 
     scrollbarSlider
         .attr('transform', function(d) {
-            return 'translate(0 ' + d.scrollbarState.topY + ')';
+            return 'translate(0 ' + (d.scrollbarState.topY || 0) + ')';
         });
 
     var scrollbarGlyph = scrollbarSlider.selectAll('.' + c.cn.scrollbarGlyph)
@@ -603,7 +605,7 @@ function headerBlock(d) {return d.type === 'header';}
  */
 
 function headerHeight(d) {
-    var headerBlocks = d.rowBlocks[0].auxiliaryBlocks;
+    var headerBlocks = d.rowBlocks.length ? d.rowBlocks[0].auxiliaryBlocks : [];
     return headerBlocks.reduce(function(p, n) {return p + rowsHeight(n, Infinity);}, 0);
 }
 
@@ -643,6 +645,7 @@ function findPagesAndCacheHeights(blocks, scrollY, scrollHeight) {
 
 function updateBlockYPosition(gd, cellsColumnBlock, tableControlView) {
     var d = flatData(cellsColumnBlock)[0];
+    if(d === undefined) return;
     var blocks = d.rowBlocks;
     var calcdata = d.calcdata;
 
diff --git a/test/jasmine/tests/table_test.js b/test/jasmine/tests/table_test.js
new file mode 100644
index 00000000000..5e936838b1d
--- /dev/null
+++ b/test/jasmine/tests/table_test.js
@@ -0,0 +1,332 @@
+var Plotly = require('@lib/index');
+var Lib = require('@src/lib');
+var Plots = require('@src/plots/plots');
+var Table = require('@src/traces/table');
+var attributes = require('@src/traces/table/attributes');
+var cn = require('@src/traces/table/constants').cn;
+
+var createGraphDiv = require('../assets/create_graph_div');
+var destroyGraphDiv = require('../assets/destroy_graph_div');
+
+var mockMulti = require('@mocks/table_latex_multitrace.json');
+
+// mock with two columns; lowest column count of general case
+var mock2 = Lib.extendDeep({}, mockMulti);
+mock2.data = [mock2.data[2]]; // keep the subplot with two columns
+
+// mock with one column as special case
+var mock1 = Lib.extendDeep({}, mock2);
+mock1.data[0].header.values = mock1.data[0].header.values.slice(0, 1);
+mock1.data[0].cells.values = mock1.data[0].cells.values.slice(0, 1);
+
+// mock with zero columns; special case, as no column can be rendered
+var mock0 = Lib.extendDeep({}, mock1);
+mock0.data[0].header.values = [];
+mock0.data[0].cells.values = [];
+
+var mock = require('@mocks/table_plain_birds.json');
+
+describe('table initialization tests', function() {
+
+    'use strict';
+
+    describe('table global defaults', function() {
+
+        it('should not coerce trace opacity', function() {
+            var gd = Lib.extendDeep({}, mock1);
+
+            Plots.supplyDefaults(gd);
+
+            expect(gd._fullData[0].opacity).toBeUndefined();
+        });
+
+        it('should use global font as label, tick and range font defaults', function() {
+            var gd = Lib.extendDeep({}, mock1);
+            delete gd.data[0].header.font;
+            delete gd.data[0].cells.font;
+            gd.layout.font = {
+                family: 'Gravitas',
+                size: 20,
+                color: 'blue'
+            };
+
+            Plots.supplyDefaults(gd);
+
+            var expected = {
+                family: 'Gravitas',
+                size: 20,
+                color: 'blue'
+            };
+
+            expect(gd._fullData[0].header.font).toEqual(expected);
+            expect(gd._fullData[0].cells.font).toEqual(expected);
+        });
+    });
+
+    describe('table defaults', function() {
+
+        function _supply(traceIn) {
+            var traceOut = { visible: true },
+                defaultColor = '#777',
+                layout = { font: {family: '"Open Sans", verdana, arial, sans-serif', size: 12, color: '#444'} };
+
+            Table.supplyDefaults(traceIn, traceOut, defaultColor, layout);
+
+            return traceOut;
+        }
+
+        it('\'line\' specification should yield a default color', function() {
+            var fullTrace = _supply({});
+            expect(fullTrace.header.fill.color).toEqual(attributes.header.fill.color.dflt);
+            expect(fullTrace.cells.fill.color).toEqual(attributes.cells.fill.color.dflt);
+        });
+
+        it('\'domain\' specification should have a default', function() {
+            var fullTrace = _supply({});
+            expect(fullTrace.domain).toEqual({x: [0, 1], y: [0, 1]});
+        });
+
+        it('\'*.values\' specification should have a default of an empty array', function() {
+            var fullTrace = _supply({});
+            expect(fullTrace.header.values).toEqual([]);
+            expect(fullTrace.cells.values).toEqual([]);
+        });
+
+        it('\'header\' should be used with default values where attributes are not provided', function() {
+            var fullTrace = _supply({
+                header: {
+                    values: ['A'],
+                    alienProperty: 'Alpha Centauri'
+                },
+                cells: {
+                    values: [1, 2], // otherwise header.values will become []
+                    alienProperty: 'Betelgeuse'
+                }
+            });
+            expect(fullTrace.header).toEqual({
+                values: ['A'], // only one column remains
+                format: [],
+                align: 'center',
+                height: 28,
+                line: { width: 1, color: 'grey' },
+                fill: { color: attributes.header.fill.color.dflt },
+                font: {family: '"Open Sans", verdana, arial, sans-serif', size: 12, color: '#444'}
+            });
+
+            expect(fullTrace.cells).toEqual({
+                values: [1, 2],
+                format: [],
+                align: 'center',
+                height: 20,
+                line: { width: 1, color: 'grey' },
+                fill: { color: attributes.cells.fill.color.dflt },
+                font: {family: '"Open Sans", verdana, arial, sans-serif', size: 12, color: '#444'}
+            });
+        });
+    });
+});
+
+describe('table', function() {
+
+    afterEach(destroyGraphDiv);
+
+    describe('edge cases', function() {
+
+        it('Works with more than one column', function(done) {
+
+            var mockCopy = Lib.extendDeep({}, mock2);
+            var gd = createGraphDiv();
+            Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() {
+                expect(gd.data.length).toEqual(1);
+                expect(gd.data[0].header.values.length).toEqual(2);
+                expect(gd.data[0].cells.values.length).toEqual(2);
+                expect(document.querySelectorAll('.' + cn.yColumn).length).toEqual(2);
+                done();
+            });
+        });
+
+        it('Works with one column', function(done) {
+
+            var mockCopy = Lib.extendDeep({}, mock1);
+            var gd = createGraphDiv();
+            Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() {
+                expect(gd.data.length).toEqual(1);
+                expect(gd.data[0].header.values.length).toEqual(1);
+                expect(gd.data[0].cells.values.length).toEqual(1);
+                expect(document.querySelectorAll('.' + cn.yColumn).length).toEqual(1);
+                done();
+            });
+        });
+
+        it('Does not error with zero columns', function(done) {
+
+            var mockCopy = Lib.extendDeep({}, mock0);
+            var gd = createGraphDiv();
+
+            Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() {
+                expect(gd.data.length).toEqual(1);
+                expect(gd.data[0].header.values.length).toEqual(0);
+                expect(gd.data[0].cells.values.length).toEqual(0);
+                expect(document.querySelectorAll('.' + cn.yColumn).length).toEqual(0);
+                done();
+            });
+        });
+
+        it('Does not raise an error with zero lines', function(done) {
+
+            var mockCopy = Lib.extendDeep({}, mock2);
+
+            mockCopy.layout.width = 320;
+            mockCopy.data[0].header.values = [[], []];
+            mockCopy.data[0].cells.values = [[], []];
+
+            var gd = createGraphDiv();
+            Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() {
+
+                expect(gd.data.length).toEqual(1);
+                expect(gd.data[0].header.values.length).toEqual(2);
+                expect(gd.data[0].cells.values.length).toEqual(2);
+                expect(document.querySelectorAll('.' + cn.yColumn).length).toEqual(2);
+                done();
+            });
+        });
+    });
+
+    describe('basic use and basic data restyling', function() {
+        var mockCopy,
+            gd;
+
+        beforeEach(function(done) {
+            mockCopy = Lib.extendDeep({}, mock);
+            mockCopy.data[0].domain = {
+                x: [0.1, 0.9],
+                y: [0.05, 0.85]
+            };
+            gd = createGraphDiv();
+            Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done);
+        });
+
+        it('`Plotly.plot` should have proper fields on `gd.data` on initial rendering', function() {
+
+            expect(gd.data.length).toEqual(1);
+            expect(gd.data[0].header.values.length).toEqual(7);
+            expect(gd.data[0].cells.values.length).toEqual(7);
+            expect(document.querySelectorAll('.' + cn.yColumn).length).toEqual(7);
+        });
+
+        it('Calling `Plotly.plot` again should add the new table trace', function(done) {
+
+            var reversedMockCopy = Lib.extendDeep({}, mockCopy);
+            reversedMockCopy.data[0].header.values = reversedMockCopy.data[0].header.values.slice().reverse();
+            reversedMockCopy.data[0].cells.values = reversedMockCopy.data[0].cells.values.slice().reverse();
+            reversedMockCopy.data[0].domain.y = [0, 0.3];
+
+            Plotly.plot(gd, reversedMockCopy.data, reversedMockCopy.layout).then(function() {
+                expect(gd.data.length).toEqual(2);
+                expect(document.querySelectorAll('.' + cn.yColumn).length).toEqual(7 * 2);
+                done();
+            });
+
+        });
+
+        it('Calling `Plotly.restyle` with a string path should amend the preexisting table', function(done) {
+
+            expect(gd.data.length).toEqual(1);
+
+            Plotly.restyle(gd, 'header.fill.color', 'magenta').then(function() {
+
+                expect(gd.data.length).toEqual(1);
+
+                expect(gd.data[0].header.fill.color).toEqual('magenta');
+                expect(gd.data[0].header.values.length).toEqual(7);
+                expect(gd.data[0].cells.values.length).toEqual(7);
+                expect(gd.data[0].header.line.color).toEqual('lightgray'); // no change relative to original mock value
+                expect(gd.data[0].cells.line.color).toEqual(['grey']); // no change relative to original mock value
+
+                done();
+            });
+
+        });
+
+        it('Calling `Plotly.restyle` for a `header.values` change should amend the preexisting one', function(done) {
+
+            function restyleValues(what, key, setterValue) {
+
+                // array values need to be wrapped in an array; unwrapping here for value comparison
+                var value = Lib.isArray(setterValue) ? setterValue[0] : setterValue;
+
+                return function() {
+                    return Plotly.restyle(gd, what + '.values[' + key + ']', setterValue).then(function() {
+                        expect(gd.data[0][what].values[key]).toEqual(value, 'for column \'' + key + '\'');
+                        expect(document.querySelectorAll('.' + cn.yColumn).length).toEqual(7);
+                    });
+                };
+            }
+
+            restyleValues('cells', 0, [['new cell content 1', 'new cell content 2']])()
+                .then(restyleValues('cells', 2, [[0, 0.1]]))
+                .then(restyleValues('header', 1, [['Species top', 'Species bottom']]))
+                .then(done);
+        });
+
+        it('Calling `Plotly.relayout` with string should amend the preexisting table', function(done) {
+            expect(gd.layout.width).toEqual(1000);
+            Plotly.relayout(gd, 'width', 500).then(function() {
+                expect(gd.data.length).toEqual(1);
+                expect(gd.layout.width).toEqual(500);
+                done();
+            });
+        });
+
+        it('Calling `Plotly.relayout` with object should amend the preexisting table', function(done) {
+            expect(gd.layout.width).toEqual(1000);
+            Plotly.relayout(gd, {width: 500}).then(function() {
+                expect(gd.data.length).toEqual(1);
+                expect(gd.layout.width).toEqual(500);
+                done();
+            });
+        });
+    });
+
+    describe('more restyling tests with scenegraph queries', function() {
+        var mockCopy,
+            gd;
+
+        beforeEach(function(done) {
+            mockCopy = Lib.extendDeep({}, mock2);
+            gd = createGraphDiv();
+            Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done);
+        });
+
+        it('Calling `Plotly.restyle` for a `header.values` change should amend the preexisting one', function(done) {
+
+            function restyleValues(what, fun, setterValue) {
+
+                var value = Lib.isArray(setterValue) ? setterValue[0] : setterValue;
+
+                return function() {
+                    return Plotly.restyle(gd, what, setterValue).then(function() {
+                        expect(fun(gd)).toEqual(value, what + ':::' + value);
+                        expect(document.querySelectorAll('.' + cn.yColumn).length).toEqual(2);
+                        expect(document.querySelectorAll('.' + cn.columnBlock).length).toEqual(6);
+                        expect(document.querySelectorAll('.' + cn.columnCell).length).toEqual(6);
+                        expect(document.querySelectorAll('.' + cn.cellRect).length).toEqual(6);
+                        expect(document.querySelectorAll('.' + cn.cellTextHolder).length).toEqual(6);
+                    });
+                };
+            }
+
+            restyleValues('cells.fill.color', function(gd) {return gd.data[0].cells.fill.color;}, [['green', 'red']])()
+                .then(restyleValues('cells.line.color', function(gd) {return gd.data[0].cells.line.color;}, [['magenta', 'blue']]))
+                .then(restyleValues('cells.line.width', function(gd) {return gd.data[0].cells.line.width;}, [[5, 3]]))
+                .then(restyleValues('cells.format', function(gd) {return gd.data[0].cells.format;}, [['', '']]))
+                .then(restyleValues('cells.prefix', function(gd) {return gd.data[0].cells.prefix;}, [['p1']]))
+                .then(restyleValues('cells.suffix', function(gd) {return gd.data[0].cells.suffix;}, [['s1']]))
+                .then(restyleValues('header.fill.color', function(gd) {return gd.data[0].header.fill.color;}, [['yellow', 'purple']]))
+                .then(restyleValues('header.line.color', function(gd) {return gd.data[0].header.line.color;}, [['green', 'red']]))
+                .then(restyleValues('header.line.width', function(gd) {return gd.data[0].header.line.width;}, [[2, 6]]))
+                .then(restyleValues('header.format', function(gd) {return gd.data[0].header.format;}, [['', '']]))
+                .then(done);
+        });
+    });
+});