Repository: jtablesaw/tablesaw
Pull request #548 (2019-07-13 15:25:24)

Subplots
Thanks for contributing.

- [x] Tick to sign-off your agreement to the [Developer Certificate of Origin (DCO) 1.1](https://developercertificate.org)

## Description

Added support for subplots as demo in the JavaScript API [here](https://plot.ly/javascript/subplots)

## Testing

A disabled test with an example on how to use the grid was added 



Patch:
diff --git a/core/pom.xml b/core/pom.xml
index 427ad5de..84a48ec9 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -79,9 +79,14 @@
                     </execution>
                 </executions>
             </plugin>
-
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <configuration>
+                    <argLine>-Duser.language=en-US -Duser.region=US</argLine>
+                </configuration>
+            </plugin>
         </plugins>
-
     </build>
 
     <dependencies>
diff --git a/core/src/main/java/tech/tablesaw/api/CategoricalColumn.java b/core/src/main/java/tech/tablesaw/api/CategoricalColumn.java
index 0325f0d7..44e44b43 100644
--- a/core/src/main/java/tech/tablesaw/api/CategoricalColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/CategoricalColumn.java
@@ -14,12 +14,12 @@
 
 package tech.tablesaw.api;
 
-import java.util.Map;
-
 import it.unimi.dsi.fastutil.objects.Object2IntMap;
 import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
 import tech.tablesaw.columns.Column;
 
+import java.util.Map;
+
 /**
  * A column type that can be summarized, or serve as a grouping variable in cross tabs or other aggregation operations.
  * <p>
@@ -47,11 +47,13 @@ public interface CategoricalColumn<T> extends Column<T> {
         final Object2IntMap<String> valueToCount = new Object2IntOpenHashMap<>();
 
         for (int i = 0; i < size(); i++) {
-            final String next = getString(i);
-            if (valueToCount.containsKey(next)) {
-                valueToCount.put(next, valueToCount.getInt(next) + 1);
-            } else {
-                valueToCount.put(next, 1);
+            if (!isMissing(i)) {
+                final String next = getString(i);
+                if (valueToCount.containsKey(next)) {
+                    valueToCount.put(next, valueToCount.getInt(next) + 1);
+                } else {
+                    valueToCount.put(next, 1);
+                }
             }
         }
         for (Map.Entry<String, Integer> entry : valueToCount.object2IntEntrySet()) {
diff --git a/core/src/main/java/tech/tablesaw/api/Table.java b/core/src/main/java/tech/tablesaw/api/Table.java
index d3de7bd2..27aef7fd 100644
--- a/core/src/main/java/tech/tablesaw/api/Table.java
+++ b/core/src/main/java/tech/tablesaw/api/Table.java
@@ -126,7 +126,7 @@ public class Table extends Relation implements Iterable<Row> {
                 try {
                     Class.forName(clazz);
                 } catch (ClassNotFoundException e) {
-                    new IllegalStateException(e);
+                    throw new IllegalStateException(e);
                 }
             }
         }
diff --git a/core/src/main/java/tech/tablesaw/columns/strings/ByteDictionaryMap.java b/core/src/main/java/tech/tablesaw/columns/strings/ByteDictionaryMap.java
index 31137cb7..35eb09cc 100644
--- a/core/src/main/java/tech/tablesaw/columns/strings/ByteDictionaryMap.java
+++ b/core/src/main/java/tech/tablesaw/columns/strings/ByteDictionaryMap.java
@@ -1,6 +1,5 @@
 package tech.tablesaw.columns.strings;
 
-import it.unimi.dsi.fastutil.bytes.Byte2IntMap;
 import it.unimi.dsi.fastutil.bytes.Byte2IntOpenHashMap;
 import it.unimi.dsi.fastutil.bytes.Byte2ObjectMap;
 import it.unimi.dsi.fastutil.bytes.Byte2ObjectOpenHashMap;
@@ -48,13 +47,16 @@ public class ByteDictionaryMap implements DictionaryMap {
 
     private final AtomicInteger nextIndex = new AtomicInteger(DEFAULT_RETURN_VALUE);
 
-    // we maintain two maps, one from strings to keys, and the second from keys to strings.
+    // we maintain 3 maps, one from strings to keys, one from keys to strings, and one from key to count of values
     private final Byte2ObjectMap<String> keyToValue = new Byte2ObjectOpenHashMap<>();
 
     private final Object2ByteOpenHashMap<String> valueToKey = new Object2ByteOpenHashMap<>();
+    
+    private final Byte2IntOpenHashMap keyToCount = new Byte2IntOpenHashMap();
 
     public ByteDictionaryMap() {
         valueToKey.defaultReturnValue(DEFAULT_RETURN_VALUE);
+        keyToCount.defaultReturnValue(0);
     }
 
     private void put(byte key, String value) {
@@ -87,16 +89,6 @@ public class ByteDictionaryMap implements DictionaryMap {
         return values.getByte(rowIndex);
     }
 
-    /**
-     * Returns true if we have seen this stringValue before, and it hasn't been removed.
-     * <p>
-     * NOTE: An answer of true does not imply that the column still contains the value, only that
-     * it is in the dictionary map
-     */
-    private boolean contains(String stringValue) {
-        return valueToKey.containsKey(stringValue);
-    }
-
     private Set<String> categories() {
         return valueToKey.keySet();
     }
@@ -129,17 +121,7 @@ public class ByteDictionaryMap implements DictionaryMap {
     }
 
     public int countOccurrences(String value) {
-        if (!contains(value)) {
-            return 0;
-        }
-        byte key = getKeyForValue(value);
-        int count = 0;
-        for (byte k : values) {
-            if (k == key) {
-                count++;
-            }
-        }
-        return count;
+        return keyToCount.get(getKeyForValue(value));
     }
 
     public Set<String> asSet() {
@@ -212,6 +194,7 @@ public class ByteDictionaryMap implements DictionaryMap {
             put(key, value);
         }
         values.add(key);
+        keyToCount.addTo(key, 1);
     }
 
     private byte getValueId() throws NoKeysAvailableException {
@@ -251,7 +234,13 @@ public class ByteDictionaryMap implements DictionaryMap {
             valueId = getValueId();
             put(valueId, str);
         }
-        values.set(rowIndex, valueId);
+        byte oldKey = values.set(rowIndex, valueId);
+        keyToCount.addTo(valueId, 1);
+        if (keyToCount.addTo(oldKey, -1) == 1) {
+            String obsoleteValue = keyToValue.remove(oldKey);
+            valueToKey.removeByte(obsoleteValue);
+            keyToCount.remove(oldKey);
+        }
     }
 
     @Override
@@ -259,6 +248,7 @@ public class ByteDictionaryMap implements DictionaryMap {
         values.clear();
         keyToValue.clear();
         valueToKey.clear();
+        keyToCount.clear();
     }
 
     @Override
@@ -273,24 +263,11 @@ public class ByteDictionaryMap implements DictionaryMap {
         Table t = Table.create("Column: " + columnName);
         StringColumn categories = StringColumn.create("Category");
         IntColumn counts = IntColumn.create("Count");
-
-        Byte2IntMap valueToCount = new Byte2IntOpenHashMap();
-
-        for (byte next : values) {
-            if (valueToCount.containsKey(next)) {
-                valueToCount.put(next, valueToCount.get(next) + 1);
-            } else {
-                valueToCount.put(next, 1);
-            }
-        }
-        for (Map.Entry<Byte, Integer> entry : valueToCount.byte2IntEntrySet()) {
+        // Now uses the keyToCount map
+        for (Map.Entry<Byte, Integer> entry : keyToCount.byte2IntEntrySet()) {
             categories.append(getValueForKey(entry.getKey()));
             counts.append(entry.getValue());
         }
-        if (countMissing() > 0) {
-            categories.append("* missing values");
-            counts.append(countMissing());
-        }
         t.addColumns(categories);
         t.addColumns(counts);
         return t;
@@ -354,13 +331,7 @@ public class ByteDictionaryMap implements DictionaryMap {
      */
     @Override
     public int countMissing() {
-        int count = 0;
-        for (int i = 0; i < size(); i++) {
-            if (MISSING_VALUE == getKeyForIndex(i)) {
-                count++;
-            }
-        }
-        return count;
+        return keyToCount.get(MISSING_VALUE);
     }
 
     @Override
diff --git a/core/src/main/java/tech/tablesaw/columns/strings/IntDictionaryMap.java b/core/src/main/java/tech/tablesaw/columns/strings/IntDictionaryMap.java
index 14ea6a1b..d4d6ecf0 100644
--- a/core/src/main/java/tech/tablesaw/columns/strings/IntDictionaryMap.java
+++ b/core/src/main/java/tech/tablesaw/columns/strings/IntDictionaryMap.java
@@ -1,6 +1,5 @@
 package tech.tablesaw.columns.strings;
 
-import it.unimi.dsi.fastutil.ints.Int2IntMap;
 import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
 import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
 import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
@@ -48,16 +47,19 @@ public class IntDictionaryMap implements DictionaryMap {
 
     private final AtomicInteger nextIndex = new AtomicInteger(DEFAULT_RETURN_VALUE);
 
-    // we maintain two maps, one from strings to keys, and the second from keys to strings.
+    // we maintain 3 maps, one from strings to keys, one from keys to strings, and one from key to count of values
     private final Int2ObjectMap<String> keyToValue = new Int2ObjectOpenHashMap<>();
 
     private final Object2IntOpenHashMap<String> valueToKey = new Object2IntOpenHashMap<>();
 
+    private final Int2IntOpenHashMap keyToCount = new Int2IntOpenHashMap();
+
     /**
      * Returns a new DictionaryMap that is a deep copy of the original
      */
     IntDictionaryMap(DictionaryMap original) throws NoKeysAvailableException {
         valueToKey.defaultReturnValue(DEFAULT_RETURN_VALUE);
+        keyToCount.defaultReturnValue(0);
 
         for (int i = 0; i < original.size(); i++) {
             String value = original.getValueForIndex(i);
@@ -95,16 +97,6 @@ public class IntDictionaryMap implements DictionaryMap {
         return values.getInt(rowIndex);
     }
 
-    /**
-     * Returns true if we have seen this stringValue before, and it hasn't been removed.
-     * <p>
-     * NOTE: An answer of true does not imply that the column still contains the value, only that
-     * it is in the dictionary map
-     */
-    private boolean contains(String stringValue) {
-        return valueToKey.containsKey(stringValue);
-    }
-
     private Set<String> categories() {
         return valueToKey.keySet();
     }
@@ -133,17 +125,7 @@ public class IntDictionaryMap implements DictionaryMap {
     }
 
     public int countOccurrences(String value) {
-        if (!contains(value)) {
-            return 0;
-        }
-        int key = getKeyForValue(value);
-        int count = 0;
-        for (int k : values) {
-            if (k == key) {
-                count++;
-            }
-        }
-        return count;
+        return keyToCount.get(getKeyForValue(value));
     }
 
     public Set<String> asSet() {
@@ -220,6 +202,7 @@ public class IntDictionaryMap implements DictionaryMap {
             put(key, value);
         }
         values.add(key);
+        keyToCount.addTo(key, 1);
     }
 
     private int getValueId() throws NoKeysAvailableException {
@@ -258,7 +241,13 @@ public class IntDictionaryMap implements DictionaryMap {
             valueId = getValueId();
             put(valueId, str);
         }
-        values.set(rowIndex, valueId);
+        int oldKey = values.set(rowIndex, valueId);
+        keyToCount.addTo(valueId, 1);
+        if (keyToCount.addTo(oldKey, -1) == 1) {
+            String obsoleteValue = keyToValue.remove(oldKey);
+            valueToKey.removeInt(obsoleteValue);
+            keyToCount.remove(oldKey);
+        }
     }
 
     @Override
@@ -266,6 +255,7 @@ public class IntDictionaryMap implements DictionaryMap {
         values.clear();
         keyToValue.clear();
         valueToKey.clear();
+        keyToCount.clear();
     }
 
     /**
@@ -275,24 +265,11 @@ public class IntDictionaryMap implements DictionaryMap {
         Table t = Table.create("Column: " + columnName);
         StringColumn categories = StringColumn.create("Category");
         IntColumn counts = IntColumn.create("Count");
-
-        Int2IntMap valueToCount = new Int2IntOpenHashMap();
-
-        for (int next : values) {
-            if (valueToCount.containsKey(next)) {
-                valueToCount.put(next, valueToCount.get(next) + 1);
-            } else {
-                valueToCount.put(next, 1);
-            }
-        }
-        for (Map.Entry<Integer, Integer> entry : valueToCount.int2IntEntrySet()) {
+        // Now uses the keyToCount map
+        for (Map.Entry<Integer, Integer> entry : keyToCount.int2IntEntrySet()) {
             categories.append(getValueForKey(entry.getKey()));
             counts.append(entry.getValue());
         }
-        if (countMissing() > 0) {
-            categories.append("* missing values");
-            counts.append(countMissing());
-        }
         t.addColumns(categories);
         t.addColumns(counts);
         return t;
@@ -356,13 +333,7 @@ public class IntDictionaryMap implements DictionaryMap {
      */
     @Override
     public int countMissing() {
-        int count = 0;
-        for (int i = 0; i < size(); i++) {
-            if (MISSING_VALUE == getKeyForIndex(i)) {
-                count++;
-            }
-        }
-        return count;
+        return keyToCount.get(MISSING_VALUE);
     }
 
     @Override
diff --git a/core/src/main/java/tech/tablesaw/columns/strings/ShortDictionaryMap.java b/core/src/main/java/tech/tablesaw/columns/strings/ShortDictionaryMap.java
index 042c53ce..deabf940 100644
--- a/core/src/main/java/tech/tablesaw/columns/strings/ShortDictionaryMap.java
+++ b/core/src/main/java/tech/tablesaw/columns/strings/ShortDictionaryMap.java
@@ -1,7 +1,6 @@
 package tech.tablesaw.columns.strings;
 
 import it.unimi.dsi.fastutil.objects.Object2ShortOpenHashMap;
-import it.unimi.dsi.fastutil.shorts.Short2IntMap;
 import it.unimi.dsi.fastutil.shorts.Short2IntOpenHashMap;
 import it.unimi.dsi.fastutil.shorts.Short2ObjectMap;
 import it.unimi.dsi.fastutil.shorts.Short2ObjectOpenHashMap;
@@ -48,16 +47,19 @@ public class ShortDictionaryMap implements DictionaryMap {
 
     private final AtomicInteger nextIndex = new AtomicInteger(DEFAULT_RETURN_VALUE);
 
-    // we maintain two maps, one from strings to keys, and the second from keys to strings.
+    // we maintain 3 maps, one from strings to keys, one from keys to strings, and one from key to count of values
     private final Short2ObjectMap<String> keyToValue = new Short2ObjectOpenHashMap<>();
 
     private final Object2ShortOpenHashMap<String> valueToKey = new Object2ShortOpenHashMap<>();
+    
+    private final Short2IntOpenHashMap keyToCount = new Short2IntOpenHashMap();
 
     /**
      * Returns a new DictionaryMap that is a deep copy of the original
      */
     ShortDictionaryMap(DictionaryMap original) throws NoKeysAvailableException {
         valueToKey.defaultReturnValue(DEFAULT_RETURN_VALUE);
+        keyToCount.defaultReturnValue(0);
 
         for (int i = 0; i < original.size(); i++) {
             String value = original.getValueForIndex(i);
@@ -95,16 +97,6 @@ public class ShortDictionaryMap implements DictionaryMap {
         return values.getShort(rowIndex);
     }
 
-    /**
-     * Returns true if we have seen this stringValue before, and it hasn't been removed.
-     * <p>
-     * NOTE: An answer of true does not imply that the column still contains the value, only that
-     * it is in the dictionary map
-     */
-    private boolean contains(String stringValue) {
-        return valueToKey.containsKey(stringValue);
-    }
-
     private Set<String> categories() {
         return valueToKey.keySet();
     }
@@ -137,17 +129,7 @@ public class ShortDictionaryMap implements DictionaryMap {
     }
 
     public int countOccurrences(String value) {
-        if (!contains(value)) {
-            return 0;
-        }
-        short key = getKeyForValue(value);
-        int count = 0;
-        for (short k : values) {
-            if (k == key) {
-                count++;
-            }
-        }
-        return count;
+        return keyToCount.get(getKeyForValue(value));
     }
 
     public Set<String> asSet() {
@@ -224,6 +206,7 @@ public class ShortDictionaryMap implements DictionaryMap {
             put(key, value);
         }
         values.add(key);
+        keyToCount.addTo(key, 1);
     }
 
     private short getValueId() throws NoKeysAvailableException {
@@ -262,7 +245,13 @@ public class ShortDictionaryMap implements DictionaryMap {
             valueId = getValueId();
             put(valueId, str);
         }
-        values.set(rowIndex, valueId);
+        short oldKey = values.set(rowIndex, valueId);
+        keyToCount.addTo(valueId, 1);
+        if (keyToCount.addTo(oldKey, -1) == 1) {
+            String obsoleteValue = keyToValue.remove(oldKey);
+            valueToKey.removeShort(obsoleteValue);
+            keyToCount.remove(oldKey);
+        }
     }
 
     @Override
@@ -270,6 +259,7 @@ public class ShortDictionaryMap implements DictionaryMap {
         values.clear();
         keyToValue.clear();
         valueToKey.clear();
+        keyToCount.clear();
     }
 
     /**
@@ -279,24 +269,11 @@ public class ShortDictionaryMap implements DictionaryMap {
         Table t = Table.create("Column: " + columnName);
         StringColumn categories = StringColumn.create("Category");
         IntColumn counts = IntColumn.create("Count");
-
-        Short2IntMap valueToCount = new Short2IntOpenHashMap();
-
-        for (short next : values) {
-            if (valueToCount.containsKey(next)) {
-                valueToCount.put(next, valueToCount.get(next) + 1);
-            } else {
-                valueToCount.put(next, 1);
-            }
-        }
-        for (Map.Entry<Short, Integer> entry : valueToCount.short2IntEntrySet()) {
+        // Now uses the keyToCount map
+        for (Map.Entry<Short, Integer> entry : keyToCount.short2IntEntrySet()) {
             categories.append(getValueForKey(entry.getKey()));
             counts.append(entry.getValue());
         }
-        if (countMissing() > 0) {
-            categories.append("* missing values");
-            counts.append(countMissing());
-        }
         t.addColumns(categories);
         t.addColumns(counts);
         return t;
@@ -360,13 +337,7 @@ public class ShortDictionaryMap implements DictionaryMap {
      */
     @Override
     public int countMissing() {
-        int count = 0;
-        for (int i = 0; i < size(); i++) {
-            if (MISSING_VALUE == getKeyForIndex(i)) {
-                count++;
-            }
-        }
-        return count;
+        return keyToCount.get(MISSING_VALUE);
     }
 
     @Override
diff --git a/core/src/main/java/tech/tablesaw/io/DataFrameWriter.java b/core/src/main/java/tech/tablesaw/io/DataFrameWriter.java
index ecb5bc05..a3a575ea 100644
--- a/core/src/main/java/tech/tablesaw/io/DataFrameWriter.java
+++ b/core/src/main/java/tech/tablesaw/io/DataFrameWriter.java
@@ -67,7 +67,7 @@ public class DataFrameWriter {
         try {
             dataWriter.write(table, new Destination(writer));
         } catch (IOException e) {
-            new IllegalStateException(e);
+            throw new IllegalStateException(e);
         }
         return writer.toString();
     }
diff --git a/excel/src/main/test/tech/tablesaw/io/xlsx/XlsxReaderTest.java b/excel/src/test/java/tech/tablesaw/io/xlsx/XlsxReaderTest.java
similarity index 100%
rename from excel/src/main/test/tech/tablesaw/io/xlsx/XlsxReaderTest.java
rename to excel/src/test/java/tech/tablesaw/io/xlsx/XlsxReaderTest.java
diff --git a/html/src/main/test/tech/tablesaw/io/html/HtmlWriterTest.java b/html/src/test/java/tech/tablesaw/io/html/HtmlWriterTest.java
similarity index 100%
rename from html/src/main/test/tech/tablesaw/io/html/HtmlWriterTest.java
rename to html/src/test/java/tech/tablesaw/io/html/HtmlWriterTest.java
diff --git a/json/src/main/test/tech/tablesaw/io/json/JsonReaderTest.java b/json/src/test/java/tech/tablesaw/io/json/JsonReaderTest.java
similarity index 100%
rename from json/src/main/test/tech/tablesaw/io/json/JsonReaderTest.java
rename to json/src/test/java/tech/tablesaw/io/json/JsonReaderTest.java
diff --git a/json/src/main/test/tech/tablesaw/io/json/JsonWriterTest.java b/json/src/test/java/tech/tablesaw/io/json/JsonWriterTest.java
similarity index 100%
rename from json/src/main/test/tech/tablesaw/io/json/JsonWriterTest.java
rename to json/src/test/java/tech/tablesaw/io/json/JsonWriterTest.java
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/components/Grid.java b/jsplot/src/main/java/tech/tablesaw/plotly/components/Grid.java
index f191ee14..1d3595ba 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/components/Grid.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/components/Grid.java
@@ -15,7 +15,22 @@ public class Grid extends Component {
     public enum RowOrder {
         ENUMERATED,
         TOP_TO_BOTTOM,
-        BOTTOM_TO_TOP
+        BOTTOM_TO_TOP;
+        
+        @Override
+        public String toString() {
+            return this.name().toLowerCase().replaceAll("_", " ");
+        }
+        
+    }
+    public enum Pattern{
+        INDEPENDENT,
+        COUPLED;
+        
+        @Override
+        public String toString() {
+            return this.name().toLowerCase();
+        }
     }
 
     /**
@@ -30,6 +45,15 @@ public class Grid extends Component {
      */
     private final RowOrder rowOrder;
 
+    /**
+     * If no `subplots`, `xaxes`, or `yaxes` are given but we do have `rows` and `columns`,',
+     * we can generate defaults using consecutive axis IDs, in two ways:',
+     * '*coupled* gives one x axis per column and one y axis per row.',
+     * '*independent* uses a new xy pair for each cell, left-to-right across each row',
+     * 'then iterating rows according to `roworder`.
+     */
+    private final Pattern pattern;
+    
     /**
      * The number of columns in the grid. If you provide a 2D `subplots` array,
      * the length of its longest row is used as the default. If you give an `xaxes` array,
@@ -57,6 +81,7 @@ public class Grid extends Component {
         this.rowOrder = gridBuilder.rowOrder;
         this.xGap = gridBuilder.xGap;
         this.yGap = gridBuilder.yGap;
+        this.pattern = gridBuilder.pattern;
     }
 
 
@@ -81,10 +106,11 @@ public class Grid extends Component {
         context.put("rows", rows);
         context.put("columns", columns);
         context.put("rowOrder", rowOrder);
+        context.put("pattern", pattern);
         return context;
     }
 
-    public GridBuilder gridBuilder() {
+    public static GridBuilder builder() {
         return new GridBuilder();
     }
 
@@ -99,6 +125,7 @@ public class Grid extends Component {
         private double yGap = 80;
 
         private RowOrder rowOrder = RowOrder.TOP_TO_BOTTOM;
+        private Pattern pattern = Pattern.COUPLED;
 
         private GridBuilder() {}
 
@@ -161,6 +188,21 @@ public class Grid extends Component {
             this.rowOrder = rowOrder;
             return this;
         }
+        
+        /**
+         * If no `subplots`, `xaxes`, or `yaxes` are given but we do have `rows` and `columns`,',
+         * we can generate defaults using consecutive axis IDs, in two ways:',
+         * '*coupled* gives one x axis per column and one y axis per row.',
+         * '*independent* uses a new xy pair for each cell, left-to-right across each row',
+         * 'then iterating rows according to `roworder`.
+         *
+         * @param pattern defaults to COUPLED
+         * @return  this GridBuilder
+         */
+        public GridBuilder pattern(Pattern pattern) {
+            this.pattern = pattern;
+            return this;
+        }
 
         public Grid build() {
             return new Grid(this);
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/components/Layout.java b/jsplot/src/main/java/tech/tablesaw/plotly/components/Layout.java
index 830ddf22..7ce52fc6 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/components/Layout.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/components/Layout.java
@@ -197,6 +197,8 @@ public class Layout {
     private final Axis yAxis4;
 
     private final Axis zAxis;
+    
+    private final Grid grid;
 
     private final BarMode barMode;
 
@@ -224,6 +226,7 @@ public class Layout {
         this.showLegend = builder.showLegend;
         this.barMode = builder.barMode;
         this.scene = builder.scene;
+        this.grid = builder.grid;
     }
 
     public String getTitle() {
@@ -282,6 +285,9 @@ public class Layout {
         if (zAxis != null) {  // TODO: remove? It's in scene for 3d scatters at least.
             context.put("zAxis", zAxis);
         }
+        if (grid != null) {
+            context.put("grid", grid);
+        }
         return context;
     }
 
@@ -407,6 +413,11 @@ public class Layout {
         private BarMode barMode = DEFAULT_BAR_MODE;
 
         private Scene scene;
+        
+        /**
+         * Define grid to use when creating subplots
+         */
+        private Grid grid;
 
         public Layout build() {
             return new Layout(this);
@@ -502,5 +513,10 @@ public class Layout {
             this.paperBgColor = color;
             return this;
         }
+        
+        public LayoutBuilder grid(Grid grid) {
+            this.grid = grid;
+            return this;
+        }
     }
 }
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/traces/AbstractTrace.java b/jsplot/src/main/java/tech/tablesaw/plotly/traces/AbstractTrace.java
index c4210418..e69d8ae2 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/traces/AbstractTrace.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/traces/AbstractTrace.java
@@ -36,20 +36,21 @@ public abstract class AbstractTrace implements Trace {
             return value;
         }
 
-        }
+    }
 
     protected final String type;
 
     private final Visibility visible;
 
     /**
-     * Determines whether or not an item corresponding to this trace is shown in the legend.
+     * Determines whether or not an item corresponding to this trace is shown in
+     * the legend.
      */
     private final boolean showLegend;
 
     /**
-     * Sets the legend group for this trace. Traces part of the same legend group hide/show at the same time
-     * when toggling legend items.
+     * Sets the legend group for this trace. Traces part of the same legend
+     * group hide/show at the same time when toggling legend items.
      */
     private final String legendGroup;
 
@@ -59,16 +60,29 @@ public abstract class AbstractTrace implements Trace {
     private final double opacity; // number between or equal to 0 and 1
 
     /**
-     * Sets the trace name. The trace name appear as the legend item and on hover.
+     * Sets the trace name. The trace name appear as the legend item and on
+     * hover.
      */
     private final String name;
 
     /**
-     * Assigns id labels to each datum. These ids for object constancy of data points during animation.
-     * Should be an array of strings, not numbers or any other type.
+     * Assigns id labels to each datum. These ids for object constancy of data
+     * points during animation. Should be an array of strings, not numbers or
+     * any other type.
      */
     private final String[] ids;
 
+    /**
+     * Sets a reference between this trace's x coordinates and a 2D cartesian x axis. 
+     * If *x* , the x coordinates refer to `layout.xaxis`. If *x2*, the x coordinates refer to `layout.xaxis2`, and so on.
+     */
+    private final String xAxis;
+    /**
+     * Sets a reference between this trace's y coordinates and a 2D cartesian y axis. 
+     * If *y* , the y coordinates refer to `layout.yaxis`. If *y2*, the y coordinates refer to `layout.yaxis2`, and so on.
+     */
+    private final String yAxis;
+
     private final HoverLabel hoverLabel;
 
     public AbstractTrace(TraceBuilder builder) {
@@ -80,6 +94,8 @@ public abstract class AbstractTrace implements Trace {
         this.ids = builder.ids;
         this.hoverLabel = builder.hoverLabel;
         this.opacity = builder.opacity;
+        this.xAxis = builder.xAxis;
+        this.yAxis = builder.yAxis;
     }
 
     @Override
@@ -105,19 +121,27 @@ public abstract class AbstractTrace implements Trace {
         Map<String, Object> context = new HashMap<>();
         context.put("type", type);
         context.put("name", name);
-        if(showLegend != DEFAULT_SHOW_LEGEND) context.put("showLegend", showLegend);
+        if (showLegend != DEFAULT_SHOW_LEGEND) {
+            context.put("showLegend", showLegend);
+        }
         context.put("legendGroup", legendGroup);
-        if(!visible.equals(DEFAULT_VISIBILITY)) context.put("visible", visible);
+        if (!visible.equals(DEFAULT_VISIBILITY)) {
+            context.put("visible", visible);
+        }
         context.put("ids", ids);
         context.put("hoverLable", hoverLabel);
-        if(opacity != DEFAULT_OPACITY) context.put("opacity", opacity);
+        if (opacity != DEFAULT_OPACITY) {
+            context.put("opacity", opacity);
+        }
+        context.put("xAxis", xAxis);
+        context.put("yAxis", yAxis);
         return context;
     }
 
     public HoverLabel hoverLabel() {
         return hoverLabel;
     }
-
+    
     public boolean showLegend() {
         return showLegend;
     }
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/traces/BarTrace.java b/jsplot/src/main/java/tech/tablesaw/plotly/traces/BarTrace.java
index c691ec0d..5a1c5c9a 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/traces/BarTrace.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/traces/BarTrace.java
@@ -108,7 +108,8 @@ public class BarTrace extends AbstractTrace {
         }
 
         /**
-         * Sets the orientation of the bars. With "v" ("h"), the value of the each bar spans along the vertical (horizontal).
+         * Sets the orientation of the bars. With "v" ("h"), the value of the
+         * each bar spans along the vertical (horizontal).
          */
         public BarBuilder orientation(Orientation orientation) {
             this.orientation = orientation;
@@ -135,6 +136,16 @@ public class BarTrace extends AbstractTrace {
             return this;
         }
 
+        public BarBuilder xAxis(String xAxis) {
+            super.xAxis(xAxis);
+            return this;
+        }
+
+        public BarBuilder yAxis(String yAxis) {
+            super.yAxis(yAxis);
+            return this;
+        }
+
         @Override
         protected String getType() {
             return type;
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/traces/BoxTrace.java b/jsplot/src/main/java/tech/tablesaw/plotly/traces/BoxTrace.java
index 4979dcfd..bc34fc55 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/traces/BoxTrace.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/traces/BoxTrace.java
@@ -82,6 +82,18 @@ public class BoxTrace extends AbstractTrace {
             return new BoxTrace(this);
         }
 
+        @Override
+        public BoxBuilder xAxis(String xAxis) {
+            super.xAxis(xAxis);
+            return this;
+        }
+
+        @Override
+        public BoxBuilder yAxis(String yAxis) {
+            super.yAxis(yAxis);
+            return this;
+        }
+
         @Override
         protected String getType() {
             return type;
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/traces/HeatmapTrace.java b/jsplot/src/main/java/tech/tablesaw/plotly/traces/HeatmapTrace.java
index b34ee400..20651806 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/traces/HeatmapTrace.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/traces/HeatmapTrace.java
@@ -67,6 +67,18 @@ public class HeatmapTrace extends AbstractTrace {
             this.z = z;
         }
 
+        @Override
+        public HeatmapBuilder xAxis(String xAxis) {
+            super.xAxis(xAxis);
+            return this;
+        }
+
+        @Override
+        public HeatmapBuilder yAxis(String yAxis) {
+            super.yAxis(yAxis);
+            return this;
+        }
+
         public HeatmapTrace build() {
             return new HeatmapTrace(this);
         }
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/traces/Histogram2DTrace.java b/jsplot/src/main/java/tech/tablesaw/plotly/traces/Histogram2DTrace.java
index 3b44230a..ed18ae71 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/traces/Histogram2DTrace.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/traces/Histogram2DTrace.java
@@ -55,12 +55,12 @@ public class Histogram2DTrace extends AbstractTrace {
     public static class Histogram2DBuilder extends TraceBuilder {
 
         private final String type = "histogram2d";
-/*
+        /*
         private int bins;
         private String barMode;
         private String histFunction;
         private String histNorm;
-*/
+         */
         private final double[] x;
         private final double[] y;
 
@@ -69,7 +69,7 @@ public class Histogram2DTrace extends AbstractTrace {
             this.y = y;
         }
 
-/*
+        /*
         public Histogram2DBuilder setBins(int bins) {
             this.bins = bins;
             return this;
@@ -89,8 +89,7 @@ public class Histogram2DTrace extends AbstractTrace {
             this.histNorm = histNorm;
             return this;
         }
-*/
-
+         */
         public Histogram2DTrace build() {
             return new Histogram2DTrace(this);
         }
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/traces/HistogramTrace.java b/jsplot/src/main/java/tech/tablesaw/plotly/traces/HistogramTrace.java
index 9245f692..aaf495d8 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/traces/HistogramTrace.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/traces/HistogramTrace.java
@@ -39,7 +39,7 @@ public class HistogramTrace extends AbstractTrace {
         this.opacity = builder.opacity;
         this.marker = builder.marker;
     }
-
+    
     @Override
     public String asJavascript(int i) {
         Writer writer = new StringWriter();
@@ -67,6 +67,7 @@ public class HistogramTrace extends AbstractTrace {
         if (marker != null) {
             context.put("marker", marker);
         }
+
         return context;
     }
 
@@ -85,8 +86,9 @@ public class HistogramTrace extends AbstractTrace {
         }
 
         /**
-         * Specifies the maximum number of desired bins. This value will be used in an algorithm that will decide
-         * the optimal bin size such that the histogram best visualizes the distribution of the data.
+         * Specifies the maximum number of desired bins. This value will be used
+         * in an algorithm that will decide the optimal bin size such that the
+         * histogram best visualizes the distribution of the data.
          */
         public HistogramBuilder nBinsX(int bins) {
             this.nBinsX = bins;
@@ -99,11 +101,12 @@ public class HistogramTrace extends AbstractTrace {
         }
 
         /**
-         * Determines whether or not the x axis bin attributes are picked by an algorithm.
-         * Note that this should be set to False if you want to manually set the number of bins using the attributes
-         * in xbins.
+         * Determines whether or not the x axis bin attributes are picked by an
+         * algorithm. Note that this should be set to False if you want to
+         * manually set the number of bins using the attributes in xbins.
          *
-         * Note also that this should be true (default) to use nbinsx to suggest a bin count
+         * Note also that this should be true (default) to use nbinsx to suggest
+         * a bin count
          */
         public HistogramBuilder autoBinX(boolean autoBinX) {
             this.autoBinX = autoBinX;
@@ -135,6 +138,16 @@ public class HistogramTrace extends AbstractTrace {
             return this;
         }
 
+        public HistogramBuilder xAxis(String xAxis) {
+            super.xAxis(xAxis);
+            return this;
+        }
+
+        public HistogramBuilder yAxis(String yAxis) {
+            super.yAxis(yAxis);
+            return this;
+        }
+
         public HistogramTrace build() {
             return new HistogramTrace(this);
         }
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/traces/Scatter3DTrace.java b/jsplot/src/main/java/tech/tablesaw/plotly/traces/Scatter3DTrace.java
index 6469d46b..2772804c 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/traces/Scatter3DTrace.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/traces/Scatter3DTrace.java
@@ -54,7 +54,9 @@ public class Scatter3DTrace extends AbstractTrace {
         context.put("x", dataAsString(x));
         context.put("z", dataAsString(z));
         context.put("showlegend", showLegend);
-        if (marker != null) context.put("marker", marker);
+        if (marker != null) {
+            context.put("marker", marker);
+        }
         if (hoverLabel != null) {
             context.put("hoverlabel", hoverLabel.asJavascript());
         }
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/traces/ScatterTrace.java b/jsplot/src/main/java/tech/tablesaw/plotly/traces/ScatterTrace.java
index 0241a7c3..21d1b44a 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/traces/ScatterTrace.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/traces/ScatterTrace.java
@@ -92,6 +92,7 @@ public class ScatterTrace extends AbstractTrace {
     public static ScatterBuilder builder(DateColumn x, NumericColumn<? extends Number> y) {
         return new ScatterBuilder(x, y);
     }
+
     public static ScatterBuilder builder(Column<?> x, NumericColumn<? extends Number> y) {
         return new ScatterBuilder(x, y);
     }
@@ -136,23 +137,43 @@ public class ScatterTrace extends AbstractTrace {
         context.put("variableName", "trace" + i);
         context.put("mode", mode);
         context.put("x", dataAsString(x));
-        if (y != null) context.put("y", dataAsString(y));
+        if (y != null) {
+            context.put("y", dataAsString(y));
+        }
 
         // for pricing data (candlesticks and OHLC)
-        if (open != null) context.put("open", dataAsString(open));
-        if (high != null) context.put("high", dataAsString(high));
-        if (low != null) context.put("low", dataAsString(low));
-        if (close != null) context.put("close", dataAsString(close));
-        if (whiskerWidth != DEFAULT_WHISKER_WIDTH) context.put("whiskerWidth", whiskerWidth);
-        if (increasing != null) context.put("increasing", increasing);
-        if (decreasing != null) context.put("increasing", decreasing);
+        if (open != null) {
+            context.put("open", dataAsString(open));
+        }
+        if (high != null) {
+            context.put("high", dataAsString(high));
+        }
+        if (low != null) {
+            context.put("low", dataAsString(low));
+        }
+        if (close != null) {
+            context.put("close", dataAsString(close));
+        }
+        if (whiskerWidth != DEFAULT_WHISKER_WIDTH) {
+            context.put("whiskerWidth", whiskerWidth);
+        }
+        if (increasing != null) {
+            context.put("increasing", increasing);
+        }
+        if (decreasing != null) {
+            context.put("increasing", decreasing);
+        }
         if (yAxis != null) {
             context.put("yAxis", yAxis);
         }
         context.put("marker", marker);
         context.put("showlegend", showLegend);
-        if (!fill.equals(DEFAULT_FILL)) context.put("fill", fill);
-        if (fillColor != null) context.put("fillColor", fillColor);
+        if (!fill.equals(DEFAULT_FILL)) {
+            context.put("fill", fill);
+        }
+        if (fillColor != null) {
+            context.put("fillColor", fillColor);
+        }
         if (hoverLabel != null) {
             context.put("hoverlabel", hoverLabel.asJavascript());
         }
@@ -220,25 +241,31 @@ public class ScatterTrace extends AbstractTrace {
         private Decreasing decreasing;
 
         /**
-         * Sets the area to fill with a solid color. Use with `fillcolor` if not "none". "tozerox" and "tozeroy"
-         * fill to x=0 and y=0 respectively. "tonextx" and "tonexty" fill between the endpoints of this trace and the
-         * endpoints of the trace before it, connecting those endpoints with straight lines (to make a stacked area graph);
-         * if there is no trace before it, they behave like "tozerox" and "tozeroy". "toself" connects the endpoints of
-         * the trace (or each segment of the trace if it has gaps) into a closed shape. "tonext" fills the space between
-         * two traces if one completely encloses the other (eg consecutive contour lines), and behaves like "toself"
-         * if there is no trace before it. "tonext" should not be used if one trace does not enclose the other.
+         * Sets the area to fill with a solid color. Use with `fillcolor` if not
+         * "none". "tozerox" and "tozeroy" fill to x=0 and y=0 respectively.
+         * "tonextx" and "tonexty" fill between the endpoints of this trace and
+         * the endpoints of the trace before it, connecting those endpoints with
+         * straight lines (to make a stacked area graph); if there is no trace
+         * before it, they behave like "tozerox" and "tozeroy". "toself"
+         * connects the endpoints of the trace (or each segment of the trace if
+         * it has gaps) into a closed shape. "tonext" fills the space between
+         * two traces if one completely encloses the other (eg consecutive
+         * contour lines), and behaves like "toself" if there is no trace before
+         * it. "tonext" should not be used if one trace does not enclose the
+         * other.
          */
         private ScatterTrace.Fill fill = DEFAULT_FILL;
 
         /**
-         * Sets the fill color. Defaults to a half-transparent variant of the line color, marker color, or marker line
-         * color, whichever is available.
+         * Sets the fill color. Defaults to a half-transparent variant of the
+         * line color, marker color, or marker line color, whichever is
+         * available.
          */
         private String fillColor;
 
         /**
-         * Sets the width of the whiskers relative to the box' width. For example, with 1, the whiskers are as wide
-         * as the box(es).
+         * Sets the width of the whiskers relative to the box' width. For
+         * example, with 1, the whiskers are as wide as the box(es).
          */
         private double whiskerWidth = DEFAULT_WHISKER_WIDTH;
 
@@ -257,10 +284,10 @@ public class ScatterTrace extends AbstractTrace {
         }
 
         private ScatterBuilder(Column<?> x,
-                               NumericColumn<? extends Number> open,
-                               NumericColumn<? extends Number> high,
-                               NumericColumn<? extends Number> low,
-                               NumericColumn<? extends Number> close) {
+                NumericColumn<? extends Number> open,
+                NumericColumn<? extends Number> high,
+                NumericColumn<? extends Number> low,
+                NumericColumn<? extends Number> close) {
             this.x = x.asObjectArray();
             this.open = open.asDoubleArray();
             this.high = high.asDoubleArray();
@@ -289,15 +316,17 @@ public class ScatterTrace extends AbstractTrace {
         }
 
         /**
-         * Sets a specific yAxis to this trace when you want to display more than one yAxis in a plot.
-         * This can be ignored if only one y axis is desired for the whole plot, and need not be set if this trace
-         * should get the default y-axis.
+         * Sets a specific yAxis to this trace when you want to display more
+         * than one yAxis in a plot. This can be ignored if only one y axis is
+         * desired for the whole plot, and need not be set if this trace should
+         * get the default y-axis.
          *
-         * There must be a corresponding Y Axis defined in the layout, e.g., if you specify YAxis.Y2 here, you must
-         * provide a value for yAxis2 in the layout
+         * There must be a corresponding Y Axis defined in the layout, e.g., if
+         * you specify YAxis.Y2 here, you must provide a value for yAxis2 in the
+         * layout
          *
-         * @param axis  The Axis to use for this trace
-         * @return      this ScatterBuilder
+         * @param axis The Axis to use for this trace
+         * @return this ScatterBuilder
          */
         public ScatterBuilder yAxis(YAxis axis) {
             this.yAxis = axis;
@@ -344,7 +373,7 @@ public class ScatterTrace extends AbstractTrace {
         /**
          * For candlestick plots
          */
-        public ScatterBuilder decreasing (Decreasing decreasing) {
+        public ScatterBuilder decreasing(Decreasing decreasing) {
             this.decreasing = decreasing;
             return this;
         }
@@ -391,5 +420,17 @@ public class ScatterTrace extends AbstractTrace {
         public ScatterBuilder hoverLabel(HoverLabel hoverLabel) {
             return (ScatterBuilder) super.hoverLabel(hoverLabel);
         }
+
+        @Override
+        public ScatterBuilder xAxis(String xAxis) {
+            super.xAxis(xAxis);
+            return this;
+        }
+
+        @Override
+        public ScatterBuilder yAxis(String yAxis) {
+            super.yAxis(yAxis);
+            return this;
+        }
     }
 }
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/traces/Trace.java b/jsplot/src/main/java/tech/tablesaw/plotly/traces/Trace.java
index e5a59152..e2327834 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/traces/Trace.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/traces/Trace.java
@@ -5,9 +5,11 @@ import tech.tablesaw.plotly.components.HoverLabel;
 public interface Trace {
 
     /**
-     * Returns a string of Javascript code that can be used to display the trace in a browser
-     * @param i     A unique number for this trace in the enclosing figure
-     * @return      A string that can be rendered in javascript
+     * Returns a string of Javascript code that can be used to display the trace
+     * in a browser
+     *
+     * @param i A unique number for this trace in the enclosing figure
+     * @return A string that can be rendered in javascript
      */
     String asJavascript(int i);
 
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/traces/TraceBuilder.java b/jsplot/src/main/java/tech/tablesaw/plotly/traces/TraceBuilder.java
index 4c6e7ff8..caf3b3a1 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/traces/TraceBuilder.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/traces/TraceBuilder.java
@@ -9,13 +9,14 @@ public abstract class TraceBuilder {
     protected AbstractTrace.Visibility visible = AbstractTrace.DEFAULT_VISIBILITY;
 
     /**
-     * Determines whether or not an item corresponding to this trace is shown in the legend.
+     * Determines whether or not an item corresponding to this trace is shown in
+     * the legend.
      */
     protected boolean showLegend = AbstractTrace.DEFAULT_SHOW_LEGEND;
 
     /**
-     * Sets the legend group for this trace. Traces part of the same legend group hide/show at the same time
-     * when toggling legend items.
+     * Sets the legend group for this trace. Traces part of the same legend
+     * group hide/show at the same time when toggling legend items.
      */
     protected String legendGroup = " ";
 
@@ -25,19 +26,37 @@ public abstract class TraceBuilder {
     protected double opacity = AbstractTrace.DEFAULT_OPACITY; // number between or equal to 0 and 1
 
     /**
-     * Sets the trace name. The trace name appear as the legend item and on hover.
+     * Sets the trace name. The trace name appear as the legend item and on
+     * hover.
      */
     protected String name;
 
     /**
-     * Assigns id labels to each datum. These ids for object constancy of data points during animation.
-     * Should be an array of strings, not numbers or any other type.
+     * Assigns id labels to each datum. These ids for object constancy of data
+     * points during animation. Should be an array of strings, not numbers or
+     * any other type.
      */
     protected String[] ids;
 
     protected HoverLabel hoverLabel;
 
-    TraceBuilder() {}
+    /**
+     * Sets a reference between this trace's x coordinates and a 2D cartesian x
+     * axis. If *x* (the default value), the x coordinates refer to
+     * `layout.xaxis`. If *x2*, the x coordinates refer to `layout.xaxis2`, and
+     * so on.
+     */
+    protected String xAxis = "x";
+    /**
+     * Sets a reference between this trace's y coordinates and a 2D cartesian y
+     * axis. If *y* (the default value), the y coordinates refer to
+     * `layout.yaxis`. If *y2*, the y coordinates refer to `layout.yaxis2`, and
+     * so on.
+     */
+    protected String yAxis = "y";
+
+    TraceBuilder() {
+    }
 
     protected abstract String getType();
 
@@ -79,4 +98,16 @@ public abstract class TraceBuilder {
         }
         return x;
     }
+
+    public TraceBuilder xAxis(String xAxis) {
+        Preconditions.checkArgument(xAxis.matches("^x[0-9]*$"));
+        this.xAxis = xAxis;
+        return this;
+    }
+
+    public TraceBuilder yAxis(String yAxis) {
+        Preconditions.checkArgument(yAxis.matches("^y[0-9]*$"));
+        this.yAxis = yAxis;
+        return this;
+    }
 }
diff --git a/jsplot/src/main/resources/grid_template.html b/jsplot/src/main/resources/grid_template.html
index e79f0093..8bd87b31 100644
--- a/jsplot/src/main/resources/grid_template.html
+++ b/jsplot/src/main/resources/grid_template.html
@@ -14,4 +14,7 @@
 {% if rowOrder is not null %}
     roworder: '{{rowOrder}}',
 {% endif %}
+{% if pattern is not null %}
+    pattern: '{{pattern}}'
+{% endif %}
 }
\ No newline at end of file
diff --git a/jsplot/src/main/resources/layout_template.html b/jsplot/src/main/resources/layout_template.html
index 57f3e624..8f9daa21 100644
--- a/jsplot/src/main/resources/layout_template.html
+++ b/jsplot/src/main/resources/layout_template.html
@@ -64,7 +64,9 @@ var layout = {
     thousandsseparator: '{{thousandsSeparator}}',
 {% endif %}
 {% if barMode is not null %}
-    barmode: '{{barMode}}'
+    barmode: '{{barMode}}',
+{% endif %}
+{% if grid is not null %}
+    grid:  {{grid | raw}}
 {% endif %}
-
 };
diff --git a/jsplot/src/main/resources/trace_template.html b/jsplot/src/main/resources/trace_template.html
index 427329a0..096c5624 100644
--- a/jsplot/src/main/resources/trace_template.html
+++ b/jsplot/src/main/resources/trace_template.html
@@ -67,6 +67,9 @@ visible: '{{visible}}',
 {% if fill is not null %}
 fill: '{{fill}}',
 {% endif %}
+{% if xAxis is not null %}
+xaxis: '{{xAxis}}',
+{% endif %}
 {% if yAxis is not null %}
 yaxis: '{{yAxis}}',
 {% endif %}


Test patch:
diff --git a/core/src/test/java/tech/tablesaw/api/CategoricalColumnTest.java b/core/src/test/java/tech/tablesaw/api/CategoricalColumnTest.java
new file mode 100644
index 00000000..b229467d
--- /dev/null
+++ b/core/src/test/java/tech/tablesaw/api/CategoricalColumnTest.java
@@ -0,0 +1,15 @@
+package tech.tablesaw.api;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class CategoricalColumnTest {
+
+    @Test
+    void testCountByCategory1() {
+        IntColumn column = IntColumn.create("test");
+        column.append(1).append(2).appendMissing().append(4);
+        assertEquals(4, column.countByCategory().nCol("Count").sum());
+    }
+}
\ No newline at end of file
diff --git a/core/src/test/java/tech/tablesaw/api/StringColumnTest.java b/core/src/test/java/tech/tablesaw/api/StringColumnTest.java
index 17ebbf26..635d6f59 100644
--- a/core/src/test/java/tech/tablesaw/api/StringColumnTest.java
+++ b/core/src/test/java/tech/tablesaw/api/StringColumnTest.java
@@ -529,4 +529,110 @@ TODO: fix
         }
         assertArrayEquals(expected, result, 0.000_000_1);
     }
+
+    @Test
+    public void countUniqueSetAfterCreateByteDict() {
+        StringColumn col = StringColumn.create("col1", 2);
+        assertEquals(1, col.countUnique(), "Wrong number of unique values after StringColumn.create");
+        assertEquals(2, col.countMissing(), "Wrong number of missing values after StringColumn.create");
+        col.set(0, "A");
+        col.set(1, "B");
+        assertEquals(2, col.countUnique(), "Wrong number of unique values after Column.set");
+        assertEquals(0, col.countMissing(), "Wrong number of missing values after Column.set");
+        col.clear();
+        assertEquals(0, col.countUnique(), "Wrong number of unique values after Column.clear");
+        assertEquals(0, col.countMissing(), "Wrong number of missing values after Column.clear");
+    }
+
+    @Test
+    public void countUniqueSetAfterAppendByteDict() {
+        StringColumn col = StringColumn.create("col1");
+        assertEquals(0, col.countUnique(), "Wrong number of unique values after StringColumn.create");
+        assertEquals(0, col.countMissing(), "Wrong number of missing values after StringColumn.create");
+        col.append("A");
+        col.append("B");
+        assertEquals(2, col.countUnique(), "Wrong number of unique values after Column.set");
+        assertEquals(0, col.countMissing(), "Wrong number of missing values after Column.set");
+        col.set(0, "C");
+        col.set(1, "D");
+        assertEquals(2, col.countUnique(), "Wrong number of unique values after Column.set");
+        assertEquals(0, col.countMissing(), "Wrong number of missing values after Column.set");
+        col.clear();
+        assertEquals(0, col.countUnique(), "Wrong number of unique values after Column.clear");
+        assertEquals(0, col.countMissing(), "Wrong number of missing values after Column.clear");
+    }
+
+    @Test
+    public void countUniqueSetAfterCreateShortDict() {
+        int size = Byte.MAX_VALUE - Byte.MIN_VALUE + 1;
+		StringColumn col = StringColumn.create("col1", size);
+        assertEquals(1, col.countUnique(), "Wrong number of unique values after StringColumn.create");
+        assertEquals(size, col.countMissing(), "Wrong number of missing values after StringColumn.create");
+        for (int i = size; --i >= 0; ) {
+            col.set(i, Integer.toString(i));
+        }
+        assertEquals(size, col.countUnique(), "Wrong number of unique values after Column.set");
+        assertEquals(0, col.countMissing(), "Wrong number of missing values after Column.set");
+        col.clear();
+        assertEquals(0, col.countUnique(), "Wrong number of unique values after Column.clear");
+        assertEquals(0, col.countMissing(), "Wrong number of missing values after Column.clear");
+    }
+
+    @Test
+    public void countUniqueSetAfterAppendShortDict() {
+        StringColumn col = StringColumn.create("col1");
+        assertEquals(0, col.countUnique(), "Wrong number of unique values after StringColumn.create");
+        assertEquals(0, col.countMissing(), "Wrong number of missing values after StringColumn.create");
+        int size = Byte.MAX_VALUE - Byte.MIN_VALUE + 1;
+        for (int i = size; --i >= 0; ) {
+            col.append(Integer.toString(i));
+        }
+        assertEquals(size, col.countUnique(), "Wrong number of unique values after Column.set");
+        assertEquals(0, col.countMissing(), "Wrong number of missing values after Column.set");
+        for (int i = size; --i >= 0; ) {
+            col.set(i, "A" + Integer.toString(i));
+        }
+        assertEquals(size, col.countUnique(), "Wrong number of unique values after Column.set");
+        assertEquals(0, col.countMissing(), "Wrong number of missing values after Column.set");
+        col.clear();
+        assertEquals(0, col.countUnique(), "Wrong number of unique values after Column.clear");
+        assertEquals(0, col.countMissing(), "Wrong number of missing values after Column.clear");
+    }
+
+    @Test
+    public void countUniqueSetAfterCreateIntDict() {
+        int size = Short.MAX_VALUE - Short.MIN_VALUE + 1;
+		StringColumn col = StringColumn.create("col1", size);
+        assertEquals(1, col.countUnique(), "Wrong number of unique values after StringColumn.create");
+        assertEquals(size, col.countMissing(), "Wrong number of missing values after StringColumn.create");
+        for (int i = size; --i >= 0; ) {
+            col.set(i, Integer.toString(i));
+        }
+        assertEquals(size, col.countUnique(), "Wrong number of unique values after Column.set");
+        assertEquals(0, col.countMissing(), "Wrong number of missing values after Column.set");
+        col.clear();
+        assertEquals(0, col.countUnique(), "Wrong number of unique values after Column.clear");
+        assertEquals(0, col.countMissing(), "Wrong number of missing values after Column.clear");
+    }
+
+    @Test
+    public void countUniqueSetAfterAppendIntDict() {
+        StringColumn col = StringColumn.create("col1");
+        assertEquals(0, col.countUnique(), "Wrong number of unique values after StringColumn.create");
+        assertEquals(0, col.countMissing(), "Wrong number of missing values after StringColumn.create");
+        int size = Short.MAX_VALUE - Short.MIN_VALUE + 1;
+        for (int i = size; --i >= 0; ) {
+            col.append(Integer.toString(i));
+        }
+        assertEquals(size, col.countUnique(), "Wrong number of unique values after Column.set");
+        assertEquals(0, col.countMissing(), "Wrong number of missing values after Column.set");
+        for (int i = size; --i >= 0; ) {
+            col.set(i, "A" + Integer.toString(i));
+        }
+        assertEquals(size, col.countUnique(), "Wrong number of unique values after Column.set");
+        assertEquals(0, col.countMissing(), "Wrong number of missing values after Column.set");
+        col.clear();
+        assertEquals(0, col.countUnique(), "Wrong number of unique values after Column.clear");
+        assertEquals(0, col.countMissing(), "Wrong number of missing values after Column.clear");
+    }
 }
diff --git a/core/src/test/java/tech/tablesaw/api/TableTest.java b/core/src/test/java/tech/tablesaw/api/TableTest.java
index 939488e8..cc209e07 100644
--- a/core/src/test/java/tech/tablesaw/api/TableTest.java
+++ b/core/src/test/java/tech/tablesaw/api/TableTest.java
@@ -214,8 +214,8 @@ public class TableTest {
         Table[] results = t.sampleSplit(.75);
         assertEquals(t.rowCount(), results[0].rowCount() + results[1].rowCount());
     }
-
-    @Test
+    
+   @Test
     public void testStratifiedSampleSplit() throws Exception {
         Table t = Table.read().csv("../data/bush.csv");
         Table[] results = t.stratifiedSampleSplit(t.stringColumn("who"), .75);
diff --git a/jsplot/src/test/java/tech/tablesaw/components/GridTest.java b/jsplot/src/test/java/tech/tablesaw/components/GridTest.java
new file mode 100644
index 00000000..dbf5bf0f
--- /dev/null
+++ b/jsplot/src/test/java/tech/tablesaw/components/GridTest.java
@@ -0,0 +1,25 @@
+package tech.tablesaw.components;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import org.junit.jupiter.api.Test;
+import tech.tablesaw.plotly.components.Grid;
+
+
+public class GridTest {
+
+    @Test
+    public void asJavascript() {
+        Grid x = Grid.builder()
+                .rows(10)
+                .columns(5)
+                .rowOrder(Grid.RowOrder.BOTTOM_TO_TOP)
+                .pattern(Grid.Pattern.INDEPENDENT)
+                .build();
+
+        String asJavascript = x.asJavascript();
+        assertTrue(asJavascript.contains("rows"));
+        assertTrue(asJavascript.contains("columns"));
+        assertTrue(asJavascript.contains("roworder"));
+        assertTrue(asJavascript.contains("pattern"));
+    }
+}
\ No newline at end of file
diff --git a/jsplot/src/test/java/tech/tablesaw/components/LayoutTest.java b/jsplot/src/test/java/tech/tablesaw/components/LayoutTest.java
index f6bf3d1d..e6c9ca6c 100644
--- a/jsplot/src/test/java/tech/tablesaw/components/LayoutTest.java
+++ b/jsplot/src/test/java/tech/tablesaw/components/LayoutTest.java
@@ -1,8 +1,10 @@
 package tech.tablesaw.components;
 
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 import tech.tablesaw.plotly.components.Axis;
+import tech.tablesaw.plotly.components.Grid;
 import tech.tablesaw.plotly.components.Layout;
 import tech.tablesaw.plotly.components.Margin;
 
@@ -31,4 +33,32 @@ public class LayoutTest {
                 .build();
         System.out.println(layout.asJavascript());
     }
+    
+    @Test
+    public void asJavascriptForGrid() {
+
+        Axis x = Axis.builder().title("x axis").build();
+        Axis y = Axis.builder().title("y axis").build();
+        Grid grid = Grid.builder().rows(2).columns(2).build();
+        Layout layout = Layout.builder()
+                .title("foobar")
+                .xAxis(x)
+                .yAxis(y)
+                .grid(grid)
+                .showLegend(true)
+                .margin(
+                        Margin.builder()
+                                .top(100)
+                                .bottom(100)
+                                .left(200)
+                                .right(200)
+                                .build())
+
+                .build();
+        String asJavascript = layout.asJavascript();
+        assertTrue(asJavascript.contains("rows"));
+        assertTrue(asJavascript.contains("columns"));
+        assertTrue(asJavascript.contains("rows"));
+        assertTrue(asJavascript.contains("xAxis"));
+    }
 }
\ No newline at end of file
diff --git a/jsplot/src/test/java/tech/tablesaw/examples/SubplotExample.java b/jsplot/src/test/java/tech/tablesaw/examples/SubplotExample.java
new file mode 100644
index 00000000..5fca30d0
--- /dev/null
+++ b/jsplot/src/test/java/tech/tablesaw/examples/SubplotExample.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package tech.tablesaw.examples;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import tech.tablesaw.api.Table;
+import tech.tablesaw.plotly.Plot;
+import tech.tablesaw.plotly.components.Figure;
+import tech.tablesaw.plotly.components.Layout;
+import tech.tablesaw.plotly.traces.Trace;
+
+import tech.tablesaw.plotly.components.Grid;
+import tech.tablesaw.plotly.traces.HistogramTrace;
+
+/**
+ * Basic sample vertical bar chart
+ */
+public class SubplotExample {
+
+    public static void main(String[] args) throws Exception {
+        Table table = Table.read().csv("../data/tornadoes_1950-2014.csv");
+        AtomicInteger order = new AtomicInteger(0);
+        Trace[] traces = table.numericColumns().stream().map(f -> {
+            int i = order.incrementAndGet();
+            return HistogramTrace.builder(f.asDoubleArray()).name(f.name()).xAxis("x"+i).yAxis("y"+i).build();
+        }).toArray(Trace[]::new);
+
+        int columns = 2;
+        Grid grid = Grid.builder().columns(columns).rows(traces.length / columns).pattern(Grid.Pattern.INDEPENDENT).build();
+        Layout layout = Layout.builder()
+                .title("Subplot")
+                .width(1000)
+                .height(700)
+                .grid(grid)
+                .build();
+
+        Plot.show(new Figure(layout, traces));
+    }
+}
diff --git a/jsplot/src/test/java/tech/tablesaw/plotly/traces/TraceBuilderTest.java b/jsplot/src/test/java/tech/tablesaw/plotly/traces/TraceBuilderTest.java
new file mode 100644
index 00000000..2c2d6c0f
--- /dev/null
+++ b/jsplot/src/test/java/tech/tablesaw/plotly/traces/TraceBuilderTest.java
@@ -0,0 +1,41 @@
+package tech.tablesaw.plotly.traces;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import org.junit.jupiter.api.Test;
+
+public class TraceBuilderTest {
+
+    
+    @Test
+    public void shouldFailPreconditionTest() {
+        TraceBuilder traceBuilder = new TraceBuilder() {
+            @Override
+            protected String getType() {
+                return "dummy";
+            }
+        };
+
+        assertThrows(IllegalArgumentException.class, () -> {
+            traceBuilder.xAxis("xa");
+        });
+        assertThrows(IllegalArgumentException.class, () -> {
+            traceBuilder.yAxis("yy");
+        });
+    }
+    
+    @Test
+    public void shouldBeAbleToSetAxis(){
+        String xAxis ="x1";
+        String yAxis = "y";
+        TraceBuilder traceBuilder = new TraceBuilder() {
+            @Override
+            protected String getType() {
+                return "dummy";
+            }
+        };
+        traceBuilder.xAxis(xAxis).yAxis(yAxis);
+        assertEquals(xAxis, traceBuilder.xAxis);
+        assertEquals(yAxis, traceBuilder.yAxis);
+    }
+}