Repository: jtablesaw/tablesaw
Pull request #497 (2019-03-20 08:42:26)

Fixes toString exception when columns are not same size.
Adds method for padding with missing values.

Thanks for contributing.

- [x] Tick to sign-off your agreement to the [Developer Certificate of Origin (DCO) 1.1](https://developercertificate.org)

## Description

Changed DataFramePrinter so it and Table.toString() does crash with varying sized columns.
Added method in Table for padding with missing values.

## Testing

Tests toString method and the padding method.


lwhite1: Hey @hallvard, are you planning to come back to this? 
I feel like I may have inadvertently insulted you or something. If so, I'm very sorry. It was not my intent. 
hallvard: > Hey @hallvard, are you planning to come back to this?
> I feel like I may have inadvertently insulted you or something. If so, I'm very sorry. It was not my intent.

Nothing like that, it's just that I've been away this week, so haven't had time to work in this. I will probably look at it this weekend. My understanding is that you prefer a method for fixing existing columns, not new ones (about to be added). But it's not high priority and the name is still undecided. In this situation I agree it's best to focus on the point of this issue: avoiding that toString() blows up.
lwhite1: > > Hey @hallvard, are you planning to come back to this?
> > I feel like I may have inadvertently insulted you or something. If so, I'm very sorry. It was not my intent.
> 
> Nothing like that, it's just that I've been away this week, so haven't had time to work in this. I will probably look at it this weekend. My understanding is that you prefer a method for fixing existing columns, not new ones (about to be added). But it's not high priority and the name is still undecided. In this situation I agree it's best to focus on the point of this issue: avoiding that toString() blows up.

That sounds good. There's no rush, but I'd have felt bad if you didn't think your contributions were appreciated. Besides @benmccann and I, I think you're our most regular contributor and it's been truly great having you on board.

Speaking of @benmccann, the only problem with him is that he's a much better programmer than I am and that can get kinda annoying. ;)  Overall all though I think it's kind of amazing how many people have contributed recently. I can barely keep up sometimes. So, thanks again for everything. 

benmccann: @lwhite1 that's very nice of you to say, but hard for me to believe since you've written most of the code :-) 
 
@hallvard I'll echo @lwhite1's sentiments and say it's been great having you contribute! Thank you so much!
lwhite1: > @lwhite1 that's very nice of you to say, but hard for me to believe since you've written most of the code :-)
That's true, but the quality would suck if you weren't involved. 

Patch:
diff --git a/README.md b/README.md
index 2202c873..da30fc38 100644
--- a/README.md
+++ b/README.md
@@ -37,21 +37,23 @@ Tablesaw supports data visualization by providing a wrapper for the Plot.ly Java
 
 ### Getting started
 
-Add tablesaw-core and tablesaw-jsplot to your project: 
+Add tablesaw-core to your project. You can find the version number for the latest release in the [release notes](https://github.com/jtablesaw/tablesaw/releases): 
 
 ```xml
 <dependency>
     <groupId>tech.tablesaw</groupId>
     <artifactId>tablesaw-core</artifactId>
-    <version>0.32.6</version>
-</dependency>
-<dependency>
-    <groupId>tech.tablesaw</groupId>
-    <artifactId>tablesaw-jsplot</artifactId>
-    <version>0.32.6</version>
+    <version>VERSION_NUMBER_GOES_HERE</version>
 </dependency>
 ```
 
+You may also add supporting projects:
+- `tablesaw-beakerx` - for using Tablesaw inside [BeakerX](http://beakerx.com/)
+- `tablesaw-excel` - for using Excel workbooks
+- `tablesaw-html` - for using HTML
+- `tablesaw-json` - for using JSON
+- `tablesaw-jsplot` - for creating charts
+
 ### Documentation and support
 
 * Start here:  https://jtablesaw.github.io/tablesaw/gettingstarted
diff --git a/aggregate/pom.xml b/aggregate/pom.xml
index a66a0363..1cd5cd37 100644
--- a/aggregate/pom.xml
+++ b/aggregate/pom.xml
@@ -5,7 +5,7 @@
   <parent>
     <groupId>tech.tablesaw</groupId>
     <artifactId>tablesaw-parent</artifactId>
-    <version>0.34.1-SNAPSHOT</version>
+    <version>0.34.2-SNAPSHOT</version>
   </parent>
 
   <!--
@@ -50,32 +50,32 @@
     <dependency>
       <groupId>tech.tablesaw</groupId>
       <artifactId>tablesaw-core</artifactId>
-      <version>0.34.1-SNAPSHOT</version>
+      <version>0.34.2-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>tech.tablesaw</groupId>
       <artifactId>tablesaw-beakerx</artifactId>
-      <version>0.34.1-SNAPSHOT</version>
+      <version>0.34.2-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>tech.tablesaw</groupId>
       <artifactId>tablesaw-excel</artifactId>
-      <version>0.34.1-SNAPSHOT</version>
+      <version>0.34.2-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>tech.tablesaw</groupId>
       <artifactId>tablesaw-html</artifactId>
-      <version>0.34.1-SNAPSHOT</version>
+      <version>0.34.2-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>tech.tablesaw</groupId>
       <artifactId>tablesaw-json</artifactId>
-      <version>0.34.1-SNAPSHOT</version>
+      <version>0.34.2-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>tech.tablesaw</groupId>
       <artifactId>tablesaw-jsplot</artifactId>
-      <version>0.34.1-SNAPSHOT</version>
+      <version>0.34.2-SNAPSHOT</version>
     </dependency>
   </dependencies>
 </project>
diff --git a/beakerx/pom.xml b/beakerx/pom.xml
index ce6da627..661b57c5 100644
--- a/beakerx/pom.xml
+++ b/beakerx/pom.xml
@@ -5,7 +5,7 @@
   <parent>
     <groupId>tech.tablesaw</groupId>
     <artifactId>tablesaw-parent</artifactId>
-    <version>0.34.1-SNAPSHOT</version>
+    <version>0.34.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>tablesaw-beakerx</artifactId>
@@ -63,7 +63,7 @@
     <dependency>
       <groupId>tech.tablesaw</groupId>
       <artifactId>tablesaw-core</artifactId>
-      <version>0.34.1-SNAPSHOT</version>
+      <version>0.34.2-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>com.github.twosigma</groupId>
diff --git a/core/pom.xml b/core/pom.xml
index ecd59071..84a48ec9 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -5,7 +5,7 @@
     <parent>
         <groupId>tech.tablesaw</groupId>
         <artifactId>tablesaw-parent</artifactId>
-        <version>0.34.1-SNAPSHOT</version>
+        <version>0.34.2-SNAPSHOT</version>
     </parent>
 
     <artifactId>tablesaw-core</artifactId>
@@ -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/DateTimeColumn.java b/core/src/main/java/tech/tablesaw/api/DateTimeColumn.java
index 11ea39d8..241223f1 100644
--- a/core/src/main/java/tech/tablesaw/api/DateTimeColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/DateTimeColumn.java
@@ -14,27 +14,7 @@
 
 package tech.tablesaw.api;
 
-import java.nio.ByteBuffer;
-import java.sql.Timestamp;
-import java.time.Instant;
-import java.time.LocalDateTime;
-import java.time.ZoneId;
-import java.time.ZoneOffset;
-import java.time.format.DateTimeFormatter;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Comparator;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Set;
-import java.util.function.Consumer;
-import java.util.function.Function;
-import java.util.function.Predicate;
-import java.util.function.Supplier;
-
 import com.google.common.base.Preconditions;
-
 import it.unimi.dsi.fastutil.ints.IntComparator;
 import it.unimi.dsi.fastutil.longs.LongArrayList;
 import it.unimi.dsi.fastutil.longs.LongArrays;
@@ -54,6 +34,26 @@ import tech.tablesaw.columns.temporal.TemporalFillers;
 import tech.tablesaw.selection.Selection;
 import tech.tablesaw.sorting.comparators.DescendingLongComparator;
 
+import java.nio.ByteBuffer;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
 /**
  * A column in a table that contains long-integer encoded (packed) local date-time values
  */
@@ -98,6 +98,23 @@ public class DateTimeColumn extends AbstractColumn<LocalDateTime>
         return column;
     }
 
+    @Override
+    public DateTimeColumn plus(long amountToAdd, ChronoUnit unit) {
+        DateTimeColumn newColumn = emptyCopy();
+        newColumn.setName(temporalColumnName(this, amountToAdd, unit));
+        DateTimeColumn column1 = this;
+
+        for (int r = 0; r < column1.size(); r++) {
+            long packedDateTime = column1.getLongInternal(r);
+            if (packedDateTime == DateTimeColumnType.missingValueIndicator()) {
+                newColumn.appendMissing();
+            } else {
+                newColumn.appendInternal(PackedLocalDateTime.plus(packedDateTime, amountToAdd, unit));
+            }
+        }
+        return newColumn;
+    }
+
     public static DateTimeColumn create(String name, LocalDateTime[] data) {
         DateTimeColumn column = new DateTimeColumn(name, new LongArrayList(data.length));
         for (LocalDateTime date : data) {
diff --git a/core/src/main/java/tech/tablesaw/api/DoubleColumn.java b/core/src/main/java/tech/tablesaw/api/DoubleColumn.java
index 95481cf3..0a9e91db 100644
--- a/core/src/main/java/tech/tablesaw/api/DoubleColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/DoubleColumn.java
@@ -30,7 +30,6 @@ import java.util.function.DoublePredicate;
 import java.util.function.DoubleSupplier;
 import java.util.function.Function;
 import java.util.function.Predicate;
-import java.util.function.ToDoubleFunction;
 
 public class DoubleColumn extends NumberColumn<Double> implements NumberFillers<DoubleColumn> {
 
@@ -277,7 +276,11 @@ public class DoubleColumn extends NumberColumn<Double> implements NumberFillers<
     public Double[] asObjectArray() {
         final Double[] output = new Double[size()];
         for (int i = 0; i < size(); i++) {
-            output[i] = getDouble(i);
+            if (!isMissing(i)) {
+                output[i] = getDouble(i);
+            } else {
+                output[i] = null;
+            }
         }
         return output;
     }
@@ -340,24 +343,6 @@ public class DoubleColumn extends NumberColumn<Double> implements NumberFillers<
         return set(row, doubleColumn.getDouble(sourceRow));
     }
 
-    /**
-     * Maps the function across all rows, appending the results to a new NumberColumn
-     *
-     * @param fun function to map
-     * @return the NumberColumn with the results
-     */
-    public DoubleColumn map(ToDoubleFunction<Double> fun) {
-        DoubleColumn result = DoubleColumn.create(name());
-        for (double t : this) {
-            try {
-                result.append(fun.applyAsDouble(t));
-            } catch (Exception e) {
-                result.appendMissing();
-            }
-        }
-        return result;
-    }
-
     /**
      * Returns a new NumberColumn with only those rows satisfying the predicate
      *
@@ -501,6 +486,14 @@ public class DoubleColumn extends NumberColumn<Double> implements NumberFillers<
         return this;
     }
 
+    @Override
+    public DoubleColumn fillWith(double d) {
+        for (int r = 0; r < size(); r++) {
+            set(r, d);
+        }
+        return this;
+    }
+
     @Override
     public DoubleColumn inRange(int start, int end) {
         return (DoubleColumn) super.inRange(start, end);
diff --git a/core/src/main/java/tech/tablesaw/api/FloatColumn.java b/core/src/main/java/tech/tablesaw/api/FloatColumn.java
index 81a0cb68..3f411555 100644
--- a/core/src/main/java/tech/tablesaw/api/FloatColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/FloatColumn.java
@@ -199,7 +199,11 @@ public class FloatColumn extends NumberColumn<Float> {
     public Float[] asObjectArray() {
         final Float[] output = new Float[size()];
         for (int i = 0; i < size(); i++) {
-            output[i] = getFloat(i);
+            if (!isMissing(i)) {
+                output[i] = getFloat(i);
+            } else {
+                output[i] = null;
+            }
         }
         return output;
     }
diff --git a/core/src/main/java/tech/tablesaw/api/InstantColumn.java b/core/src/main/java/tech/tablesaw/api/InstantColumn.java
index 59cdffc4..2bf9912a 100644
--- a/core/src/main/java/tech/tablesaw/api/InstantColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/InstantColumn.java
@@ -14,26 +14,7 @@
 
 package tech.tablesaw.api;
 
-import java.nio.ByteBuffer;
-import java.sql.Timestamp;
-import java.time.Instant;
-import java.time.LocalDateTime;
-import java.time.ZoneId;
-import java.time.ZoneOffset;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Comparator;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Set;
-import java.util.function.Consumer;
-import java.util.function.Function;
-import java.util.function.Predicate;
-import java.util.function.Supplier;
-
 import com.google.common.base.Preconditions;
-
 import it.unimi.dsi.fastutil.ints.IntComparator;
 import it.unimi.dsi.fastutil.longs.LongArrayList;
 import it.unimi.dsi.fastutil.longs.LongArrays;
@@ -46,18 +27,37 @@ import tech.tablesaw.columns.AbstractColumnParser;
 import tech.tablesaw.columns.Column;
 import tech.tablesaw.columns.instant.InstantColumnFormatter;
 import tech.tablesaw.columns.instant.InstantColumnType;
+import tech.tablesaw.columns.instant.InstantMapFunctions;
 import tech.tablesaw.columns.instant.PackedInstant;
 import tech.tablesaw.columns.temporal.TemporalFillers;
 import tech.tablesaw.columns.temporal.TemporalFilters;
-import tech.tablesaw.columns.temporal.TemporalMapFunctions;
 import tech.tablesaw.selection.Selection;
 import tech.tablesaw.sorting.comparators.DescendingLongComparator;
 
+import java.nio.ByteBuffer;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
 /**
  * A column in a table that contains long-integer encoded (packed) local date-time values
  */
 public class InstantColumn extends AbstractColumn<Instant>
-    implements TemporalMapFunctions<Instant>, TemporalFillers<Instant, InstantColumn>,
+    implements InstantMapFunctions, TemporalFillers<Instant, InstantColumn>,
         TemporalFilters<Instant>, CategoricalColumn<Instant> {
 
     private final LongComparator reverseLongComparator = DescendingLongComparator.instance();
@@ -114,6 +114,24 @@ public class InstantColumn extends AbstractColumn<Instant>
         return valueIsMissing(getLongInternal(rowNumber));
     }
 
+    @Override
+    public InstantColumn plus(long amountToAdd, ChronoUnit unit) {
+        InstantColumn newColumn = emptyCopy();
+        newColumn.setName(temporalColumnName(this, amountToAdd, unit));
+        InstantColumn column1 = this;
+
+        for (int r = 0; r < column1.size(); r++) {
+            long packedDateTime = column1.getLongInternal(r);
+            if (packedDateTime == InstantColumnType.missingValueIndicator()) {
+                newColumn.appendMissing();
+            } else {
+                newColumn.appendInternal(PackedInstant.plus(packedDateTime, amountToAdd, unit));
+            }
+        }
+        return newColumn;
+    }
+
+
     @Override
     public InstantColumn subset(final int[] rows) {
         final InstantColumn c = this.emptyCopy();
diff --git a/core/src/main/java/tech/tablesaw/api/IntColumn.java b/core/src/main/java/tech/tablesaw/api/IntColumn.java
index 6560e8cf..02ad14eb 100644
--- a/core/src/main/java/tech/tablesaw/api/IntColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/IntColumn.java
@@ -209,7 +209,11 @@ public class IntColumn extends NumberColumn<Integer> implements CategoricalColum
     public Integer[] asObjectArray() {
         final Integer[] output = new Integer[size()];
         for (int i = 0; i < size(); i++) {
-            output[i] = getInt(i);
+            if (!isMissing(i)) {
+                output[i] = getInt(i);
+            } else {
+                output[i] = null;
+            }
         }
         return output;
     }
diff --git a/core/src/main/java/tech/tablesaw/api/LongColumn.java b/core/src/main/java/tech/tablesaw/api/LongColumn.java
index 61152423..8dfb6696 100644
--- a/core/src/main/java/tech/tablesaw/api/LongColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/LongColumn.java
@@ -245,7 +245,11 @@ public class LongColumn extends NumberColumn<Long> implements CategoricalColumn<
     public Long[] asObjectArray() {
         final Long[] output = new Long[size()];
         for (int i = 0; i < size(); i++) {
-            output[i] = getLong(i);
+            if (!isMissing(i)) {
+                output[i] = getLong(i);
+            } else {
+                output[i] = null;
+            }
         }
         return output;
     }
diff --git a/core/src/main/java/tech/tablesaw/api/NumericColumn.java b/core/src/main/java/tech/tablesaw/api/NumericColumn.java
index 04546882..4bb9eed2 100644
--- a/core/src/main/java/tech/tablesaw/api/NumericColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/NumericColumn.java
@@ -1,52 +1,33 @@
 package tech.tablesaw.api;
 
-import static tech.tablesaw.aggregate.AggregateFunctions.geometricMean;
-import static tech.tablesaw.aggregate.AggregateFunctions.kurtosis;
-import static tech.tablesaw.aggregate.AggregateFunctions.max;
-import static tech.tablesaw.aggregate.AggregateFunctions.mean;
-import static tech.tablesaw.aggregate.AggregateFunctions.median;
-import static tech.tablesaw.aggregate.AggregateFunctions.min;
-import static tech.tablesaw.aggregate.AggregateFunctions.populationVariance;
-import static tech.tablesaw.aggregate.AggregateFunctions.product;
-import static tech.tablesaw.aggregate.AggregateFunctions.quadraticMean;
-import static tech.tablesaw.aggregate.AggregateFunctions.quartile1;
-import static tech.tablesaw.aggregate.AggregateFunctions.quartile3;
-import static tech.tablesaw.aggregate.AggregateFunctions.range;
-import static tech.tablesaw.aggregate.AggregateFunctions.skewness;
-import static tech.tablesaw.aggregate.AggregateFunctions.stdDev;
-import static tech.tablesaw.aggregate.AggregateFunctions.sum;
-import static tech.tablesaw.aggregate.AggregateFunctions.sumOfLogs;
-import static tech.tablesaw.aggregate.AggregateFunctions.sumOfSquares;
-import static tech.tablesaw.aggregate.AggregateFunctions.variance;
-import static tech.tablesaw.columns.numbers.NumberPredicates.isMissing;
-import static tech.tablesaw.columns.numbers.NumberPredicates.isNotMissing;
-
-import java.util.Arrays;
-import java.util.Optional;
-import java.util.function.BiPredicate;
-import java.util.function.DoubleBinaryOperator;
-import java.util.function.DoubleFunction;
-import java.util.function.DoublePredicate;
-
+import it.unimi.dsi.fastutil.doubles.DoubleComparator;
+import it.unimi.dsi.fastutil.doubles.DoubleRBTreeSet;
 import org.apache.commons.math3.exception.NotANumberException;
 import org.apache.commons.math3.stat.correlation.KendallsCorrelation;
 import org.apache.commons.math3.stat.correlation.PearsonsCorrelation;
 import org.apache.commons.math3.stat.correlation.SpearmansCorrelation;
-
-import it.unimi.dsi.fastutil.doubles.DoubleComparator;
-import it.unimi.dsi.fastutil.doubles.DoubleRBTreeSet;
 import tech.tablesaw.aggregate.AggregateFunctions;
 import tech.tablesaw.aggregate.NumericAggregateFunction;
 import tech.tablesaw.columns.Column;
+import tech.tablesaw.columns.numbers.NumberColumnFormatter;
 import tech.tablesaw.columns.numbers.NumberFilters;
 import tech.tablesaw.columns.numbers.NumberMapFunctions;
 import tech.tablesaw.columns.numbers.NumberRollingColumn;
 import tech.tablesaw.columns.numbers.Stats;
-import tech.tablesaw.filtering.predicates.DoubleBiPredicate;
-import tech.tablesaw.filtering.predicates.DoubleRangePredicate;
 import tech.tablesaw.selection.BitmapBackedSelection;
 import tech.tablesaw.selection.Selection;
 
+import java.text.NumberFormat;
+import java.util.Arrays;
+import java.util.Optional;
+import java.util.function.BiPredicate;
+import java.util.function.DoubleBinaryOperator;
+import java.util.function.DoubleFunction;
+import java.util.function.DoublePredicate;
+
+import static tech.tablesaw.aggregate.AggregateFunctions.*;
+import static tech.tablesaw.columns.numbers.NumberPredicates.*;
+
 public interface NumericColumn<T> extends Column<T>, NumberMapFunctions, NumberFilters {
 
     @Override
@@ -75,30 +56,6 @@ public interface NumericColumn<T> extends Column<T>, NumberMapFunctions, NumberF
         return bitmap;
     }
 
-    @Override
-    default Selection eval(final DoubleBiPredicate predicate, final DoubleColumn otherColumn) {
-        final Selection selection = new BitmapBackedSelection();
-        for (int idx = 0; idx < size(); idx++) {
-            if (predicate.test(getDouble(idx), otherColumn.getDouble(idx))) {
-                selection.add(idx);
-            }
-        }
-        return selection;
-    }
-
-    @Override
-    default Selection eval(final DoubleBiPredicate predicate, final Number number) {
-        final double value = number.doubleValue();
-        final Selection bitmap = new BitmapBackedSelection();
-        for (int idx = 0; idx < size(); idx++) {
-            final double next = getDouble(idx);
-            if (predicate.test(next, value)) {
-                bitmap.add(idx);
-            }
-        }
-        return bitmap;
-    }
-
     @Override
     default Selection eval(final BiPredicate<Number, Number> predicate, final Number number) {
         final double value = number.doubleValue();
@@ -112,20 +69,6 @@ public interface NumericColumn<T> extends Column<T>, NumberMapFunctions, NumberF
         return bitmap;
     }
 
-    @Override
-    default Selection eval(final DoubleRangePredicate predicate, final Number rangeStart, final Number rangeEnd) {
-        final double start = rangeStart.doubleValue();
-        final double end = rangeEnd.doubleValue();
-        final Selection bitmap = new BitmapBackedSelection();
-        for (int idx = 0; idx < size(); idx++) {
-            final double next = getDouble(idx);
-            if (predicate.test(next, start, end)) {
-                bitmap.add(idx);
-            }
-        }
-        return bitmap;
-    }
-
     @Override
     default Selection isIn(final Number... numbers) {
         return isIn(Arrays.stream(numbers).mapToDouble(Number::doubleValue).toArray());
@@ -241,6 +184,10 @@ public interface NumericColumn<T> extends Column<T>, NumberMapFunctions, NumberF
         return (first ? Optional.<Double>empty() : Optional.<Double>of(d1));
     }
 
+    void setPrintFormatter(final NumberFormat format, final String missingValueString);
+
+    void setPrintFormatter(final NumberColumnFormatter formatter);
+
     /**
      * Reduction with binary operator and initial value
      *
@@ -284,7 +231,7 @@ public interface NumericColumn<T> extends Column<T>, NumberMapFunctions, NumberF
      * @param into Column to which results are appended
      * @return the provided Column, to which results are appended
      */
-    default <R> Column<R> mapInto(DoubleFunction<? extends R> fun, Column<R> into) {
+    default <R extends Column<RT>, RT> R mapInto(DoubleFunction<? extends RT> fun, R into) {
         for (int i = 0; i < size(); i++) {
             try {
                 into.append(fun.apply(getDouble(i)));
diff --git a/core/src/main/java/tech/tablesaw/api/ShortColumn.java b/core/src/main/java/tech/tablesaw/api/ShortColumn.java
index 4a040a02..0516f6a3 100644
--- a/core/src/main/java/tech/tablesaw/api/ShortColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/ShortColumn.java
@@ -200,7 +200,11 @@ public class ShortColumn extends NumberColumn<Short> implements CategoricalColum
     public Short[] asObjectArray() {
         final Short[] output = new Short[size()];
         for (int i = 0; i < size(); i++) {
-            output[i] = getShort(i);
+            if (!isMissing(i)) {
+                output[i] = getShort(i);
+            } else {
+                output[i] = null;
+            }
         }
         return output;
     }
diff --git a/core/src/main/java/tech/tablesaw/api/Table.java b/core/src/main/java/tech/tablesaw/api/Table.java
index afd3d94c..e075413a 100644
--- a/core/src/main/java/tech/tablesaw/api/Table.java
+++ b/core/src/main/java/tech/tablesaw/api/Table.java
@@ -14,21 +14,9 @@
 
 package tech.tablesaw.api;
 
-import static java.util.stream.Collectors.toList;
-import static tech.tablesaw.aggregate.AggregateFunctions.countMissing;
-import static tech.tablesaw.selection.Selection.selectNRowsAtRandom;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Comparator;
-import java.util.Iterator;
-import java.util.List;
-import java.util.function.Consumer;
-import java.util.function.Predicate;
-
 import com.google.common.base.Preconditions;
+import com.google.common.collect.Streams;
 import com.google.common.primitives.Ints;
-
 import io.github.classgraph.ClassGraph;
 import io.github.classgraph.ScanResult;
 import it.unimi.dsi.fastutil.ints.IntArrays;
@@ -55,6 +43,19 @@ import tech.tablesaw.table.Rows;
 import tech.tablesaw.table.StandardTableSliceGroup;
 import tech.tablesaw.table.TableSliceGroup;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+
+import static java.util.stream.Collectors.toList;
+import static tech.tablesaw.aggregate.AggregateFunctions.countMissing;
+import static tech.tablesaw.selection.Selection.selectNRowsAtRandom;
+
 /**
  * A table of data, consisting of some number of columns, each of which has the same number of rows.
  * All the data in a column has the same type: integer, float, category, etc., but a table may contain an arbitrary
@@ -124,7 +125,7 @@ public class Table extends Relation implements Iterable<Row> {
                 try {
                     Class.forName(clazz);
                 } catch (ClassNotFoundException e) {
-                    new IllegalStateException(e);
+                    throw new IllegalStateException(e);
                 }
             }
         }
@@ -454,6 +455,28 @@ public class Table extends Relation implements Iterable<Row> {
         tables[1] = where(table2Selection);
         return tables;
     }
+    
+    /**
+     * Splits the table into two stratified samples, this uses the specified column to divide the table into groups, randomly assigning records to each according to the proportion given in trainingProportion. 
+     * @param column the column to be used for the stratified sampling
+     * @param table1Proportion The proportion to go in the first table
+     * @return An array two tables, with the first table having the proportion specified in the method parameter,
+     * and the second table having the balance of the rows
+     */
+    public Table[] stratifiedSampleSplit(CategoricalColumn<?> column, double table1Proportion) {
+        Preconditions.checkArgument(containsColumn(column),
+                "The categorical column must be part of the table, you can create a string column and add it to this table before sampling.");
+        final Table first = emptyCopy();
+        final Table second = emptyCopy();
+        
+        splitOn(column).asTableList().forEach(tab-> {
+            Table[] splits = tab.sampleSplit(table1Proportion); 
+            first.append(splits[0]);
+            second.append(splits[1]);
+        });
+        
+        return new Table[]{ first, second };
+    }
 
     /**
      * Returns a table consisting of randomly selected records from this table. The sample size is based on the
@@ -669,11 +692,33 @@ public class Table extends Relation implements Iterable<Row> {
         return where(selection);
     }
 
+    /**
+     * Retains the first rowCount rows if rowCount positive.
+     * Retains the last rowCount rows if rowCount negative.
+     */
+    public Table inRange(int rowCount) {
+        Preconditions.checkArgument(rowCount <= rowCount());
+        int rowStart = rowCount >= 0 ? 0 : rowCount() + rowCount;
+        int rowEnd = rowCount >= 0 ? rowCount : rowCount();
+        return where(Selection.withRange(rowStart, rowEnd));
+    }
+
     public Table inRange(int rowStart, int rowEnd) {
         Preconditions.checkArgument(rowEnd <= rowCount());
         return where(Selection.withRange(rowStart, rowEnd));
     }
 
+    /**
+     * Drops the first rowCount rows if rowCount positive.
+     * Drops the last rowCount rows if rowCount negative.
+     */
+    public Table dropRange(int rowCount) {
+        Preconditions.checkArgument(rowCount <= rowCount());
+        int rowStart = rowCount >= 0 ? rowCount : 0;
+        int rowEnd = rowCount >= 0 ? rowCount() : rowCount() + rowCount;
+        return where(Selection.withRange(rowStart, rowEnd));
+    }
+
     public Table dropRange(int rowStart, int rowEnd) {
         Preconditions.checkArgument(rowEnd <= rowCount());
         return where(Selection.withoutRange(0, rowCount(), rowStart, rowEnd));
@@ -867,6 +912,26 @@ public class Table extends Relation implements Iterable<Row> {
         return this;
     }
 
+    /**
+     * Appends an empty row and returns a Row object indexed to the newly added row so values can be
+     * set.
+     *
+     * Intended usage:
+     *
+     *    for (int i = 0; ...) {
+     *        Row row = table.appendRow();
+     *        row.setString("name", "Bob");
+     *        row.setFloat("IQ", 123.4f);
+     *        ...etc.
+     *    }
+     */
+    public Row appendRow() {
+        for (final Column<?> column : columnList) {
+            column.appendMissing();
+        }
+        return row(rowCount() - 1);
+    }
+
     /**
      * Add all the columns of tableToConcatenate to this table
      * Note: The columns in the result must have unique names, when compared case insensitive
@@ -970,6 +1035,20 @@ public class Table extends Relation implements Iterable<Row> {
         return groupingColumn.countByCategory();
     }
 
+    /**
+     * Returns a table containing two columns, the grouping column, and a column named "Count" that contains
+     * the counts for each grouping column value
+     *
+     * @param categoricalColumnName The name of a CategoricalColumn in this table
+     * @return  A table containing counts of rows grouped by the categorical column
+     * @throws  ClassCastException if the categoricalColumnName parameter is the name of a column that does not
+     *      * implement categorical
+     */
+    public Table countBy(String categoricalColumnName)  {
+        CategoricalColumn<?> groupingColumn = categoricalColumn(categoricalColumnName);
+        return groupingColumn.countByCategory();
+    }
+
     /**
      * Returns a new DataFrameJoiner initialized with multiple {@code columnNames}
      * @param columnNames   Name of the columns to join on.
@@ -1002,27 +1081,24 @@ public class Table extends Relation implements Iterable<Row> {
         };
     }
 
+    public Stream<Row> stream() {
+        return Streams.stream(iterator());
+    }
+
     /**
      * Applies the operation in {@code doable} to every row in the table
+     * @deprecated use {@code stream().forEach}
      */
     public void doWithRows(Consumer<Row> doable) {
-        Row row = new Row(this);
-        while (row.hasNext()) {
-            doable.accept(row.next());
-        }
+        stream().forEach(doable);
     }
 
     /**
      * Applies the predicate to each row, and return true if any row returns true
+     * @deprecated use {@code stream().anyMatch}
      */
     public boolean detect(Predicate<Row> predicate) {
-        Row row = new Row(this);
-        while (row.hasNext()) {
-            if (predicate.test(row.next())) {
-                return true;
-            }
-        }
-        return false;
+        return stream().anyMatch(predicate);
     }
 
     /**
@@ -1136,4 +1212,4 @@ public class Table extends Relation implements Iterable<Row> {
             throw new UnsupportedOperationException("This Pairs function returns no results");
         }
     }
-}
\ No newline at end of file
+}
diff --git a/core/src/main/java/tech/tablesaw/columns/AbstractColumn.java b/core/src/main/java/tech/tablesaw/columns/AbstractColumn.java
index 07b9a498..1395659d 100644
--- a/core/src/main/java/tech/tablesaw/columns/AbstractColumn.java
+++ b/core/src/main/java/tech/tablesaw/columns/AbstractColumn.java
@@ -14,8 +14,24 @@
 
 package tech.tablesaw.columns;
 
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.function.Function;
+import tech.tablesaw.api.BooleanColumn;
 import tech.tablesaw.api.ColumnType;
+import tech.tablesaw.api.DateColumn;
+import tech.tablesaw.api.DateTimeColumn;
+import tech.tablesaw.api.DoubleColumn;
+import tech.tablesaw.api.FloatColumn;
+import tech.tablesaw.api.InstantColumn;
+import tech.tablesaw.api.IntColumn;
+import tech.tablesaw.api.LongColumn;
+import tech.tablesaw.api.ShortColumn;
 import tech.tablesaw.api.StringColumn;
+import tech.tablesaw.api.TextColumn;
+import tech.tablesaw.api.TimeColumn;
 
 /**
  * Partial implementation of the {@link Column} interface
@@ -65,4 +81,184 @@ public abstract class AbstractColumn<T> implements Column<T> {
         }
         return sc;
     }
+
+    /**
+     * Maps the function across all rows, appending the results to a new BooleanColumn.
+     *
+     * Ignores missing values.
+     *
+     * @param name of the new column.
+     * @param function to map current column into BooleanColumn.
+     * @return BooleanColumn with the results appended.
+     */
+    public BooleanColumn mapToBooleanColumn(String name, Function<? super T, ? extends Boolean> function) {
+        BooleanColumn newColumn = BooleanColumn.create(name, size());
+        mapInto(function, newColumn);
+        return newColumn;
+    }
+
+    /**
+     * Maps the function across all rows, appending the results to a new DateColumn.
+     *
+     * Ignores missing values.
+     *
+     * @param name of the new column.
+     * @param function to map current column into DateColumn.
+     * @return DateColumn with the results appended.
+     */
+    public DateColumn mapToDateColumn(String name, Function<? super T, ? extends LocalDate> function) {
+        DateColumn newColumn = DateColumn.create(name, size());
+        mapInto(function, newColumn);
+        return newColumn;
+    }
+
+    /**
+     * Maps the function across all rows, appending the results to a new DateTimeColumn.
+     *
+     * Ignores missing values.
+     *
+     * @param name of the new column.
+     * @param function to map current column into DateTimeColumn.
+     * @return DateTimeColumn with the results appended.
+     */
+    public DateTimeColumn mapToDateTimeColumn(String name, Function<? super T, ? extends LocalDateTime> function) {
+        DateTimeColumn newColumn = DateTimeColumn.create(name, size());
+        mapInto(function, newColumn);
+        return newColumn;
+    }
+
+    /**
+     * Maps the function across all rows, appending the results to a new DoubleColumn.
+     *
+     * Ignores missing values.
+     *
+     * @param name of the new column.
+     * @param function to map current column into DoubleColumn.
+     * @return DoubleColumn with the results appended.
+     */
+    public DoubleColumn mapToDoubleColumn(String name, Function<? super T, ? extends Double> function) {
+        DoubleColumn newColumn = DoubleColumn.create(name, size());
+        mapInto(function, newColumn);
+        return newColumn;
+    }
+
+    /**
+     * Maps the function across all rows, appending the results to a new FloatColumn.
+     *
+     * Ignores missing values.
+     *
+     * @param name of the new column.
+     * @param function to map current column into FloatColumn.
+     * @return FloatColumn with the results appended.
+     */
+    public FloatColumn mapToFloatColumn(String name, Function<? super T, ? extends Float> function) {
+        FloatColumn newColumn = FloatColumn.create(name, size());
+        mapInto(function, newColumn);
+        return newColumn;
+    }
+
+    /**
+     * Maps the function across all rows, appending the results to a new InstantColumn.
+     *
+     * Ignores missing values.
+     *
+     * @param name of the new column.
+     * @param function to map current column into InstantColumn.
+     * @return InstantColumn with the results appended.
+     */
+    public InstantColumn mapToInstantColumn(String name, Function<? super T, ? extends Instant> function) {
+        InstantColumn newColumn = InstantColumn.create(name, size());
+        mapInto(function, newColumn);
+        return newColumn;
+    }
+
+    /**
+     * Maps the function across all rows, appending the results to a new IntColumn.
+     *
+     * Ignores missing values.
+     *
+     * @param name of the new column.
+     * @param function to map current column into IntColumn.
+     * @return IntColumn with the results appended.
+     */
+    public IntColumn mapToIntColumn(String name, Function<? super T, ? extends Integer> function) {
+        IntColumn newColumn = IntColumn.create(name, size());
+        mapInto(function, newColumn);
+        return newColumn;
+    }
+
+    /**
+     * Maps the function across all rows, appending the results to a new LongColumn.
+     *
+     * Ignores missing values.
+     *
+     * @param name of the new column.
+     * @param function to map current column into LongColumn.
+     * @return LongColumn with the results appended.
+     */
+    public LongColumn mapToLongColumn(String name, Function<? super T, ? extends Long> function) {
+        LongColumn newColumn = LongColumn.create(name, size());
+        mapInto(function, newColumn);
+        return newColumn;
+    }
+
+    /**
+     * Maps the function across all rows, appending the results to a new ShortColumn.
+     *
+     * Ignores missing values.
+     *
+     * @param name of the new column.
+     * @param function to map current column into ShortColumn.
+     * @return ShortColumn with the results appended.
+     */
+    public ShortColumn mapToShortColumn(String name, Function<? super T, ? extends Short> function) {
+        ShortColumn newColumn = ShortColumn.create(name, size());
+        mapInto(function, newColumn);
+        return newColumn;
+    }
+
+    /**
+     * Maps the function across all rows, appending the results to a new StringColumn.
+     *
+     * Ignores missing values.
+     *
+     * @param name of the new column.
+     * @param function to map current column into StringColumn.
+     * @return StringColumn with the results appended.
+     */
+    public StringColumn mapToStringColumn(String name, Function<? super T, ? extends String> function) {
+        StringColumn newColumn = StringColumn.create(name, size());
+        mapInto(function, newColumn);
+        return newColumn;
+    }
+
+    /**
+     * Maps the function across all rows, appending the results to a new TextColumn.
+     *
+     * Ignores missing values.
+     *
+     * @param name of the new column.
+     * @param function to map current column into TextColumn.
+     * @return TextColumn with the results appended.
+     */
+    public TextColumn mapToTextColumn(String name, Function<? super T, ? extends String> function) {
+        TextColumn newColumn = TextColumn.create(name, size());
+        mapInto(function, newColumn);
+        return newColumn;
+    }
+
+    /**
+     * Maps the function across all rows, appending the results to a new TimeColumn.
+     *
+     * Ignores missing values.
+     *
+     * @param name of the new column.
+     * @param function to map current column into TimeColumn.
+     * @return TimeColumn with the results appended.
+     */
+    public TimeColumn mapToTimeColumn(String name, Function<? super T, ? extends LocalTime> function) {
+        TimeColumn newColumn = TimeColumn.create(name, size());
+        mapInto(function, newColumn);
+        return newColumn;
+    }
 }
\ No newline at end of file
diff --git a/core/src/main/java/tech/tablesaw/columns/datetimes/DateTimeMapFunctions.java b/core/src/main/java/tech/tablesaw/columns/datetimes/DateTimeMapFunctions.java
index 507b8798..fbee85b3 100644
--- a/core/src/main/java/tech/tablesaw/columns/datetimes/DateTimeMapFunctions.java
+++ b/core/src/main/java/tech/tablesaw/columns/datetimes/DateTimeMapFunctions.java
@@ -14,32 +14,7 @@
 
 package tech.tablesaw.columns.datetimes;
 
-import static tech.tablesaw.columns.datetimes.PackedLocalDateTime.getDayOfMonth;
-import static tech.tablesaw.columns.datetimes.PackedLocalDateTime.getDayOfWeek;
-import static tech.tablesaw.columns.datetimes.PackedLocalDateTime.getDayOfYear;
-import static tech.tablesaw.columns.datetimes.PackedLocalDateTime.getHour;
-import static tech.tablesaw.columns.datetimes.PackedLocalDateTime.getMinute;
-import static tech.tablesaw.columns.datetimes.PackedLocalDateTime.getMinuteOfDay;
-import static tech.tablesaw.columns.datetimes.PackedLocalDateTime.getMonthValue;
-import static tech.tablesaw.columns.datetimes.PackedLocalDateTime.getQuarter;
-import static tech.tablesaw.columns.datetimes.PackedLocalDateTime.getSecondOfDay;
-import static tech.tablesaw.columns.datetimes.PackedLocalDateTime.getWeekOfYear;
-import static tech.tablesaw.columns.datetimes.PackedLocalDateTime.getYear;
-import static tech.tablesaw.columns.datetimes.PackedLocalDateTime.monthsUntil;
-import static tech.tablesaw.columns.datetimes.PackedLocalDateTime.pack;
-import static tech.tablesaw.columns.datetimes.PackedLocalDateTime.yearsUntil;
-import static tech.tablesaw.columns.instant.PackedInstant.daysUntil;
-import static tech.tablesaw.columns.instant.PackedInstant.hoursUntil;
-import static tech.tablesaw.columns.instant.PackedInstant.minutesUntil;
-import static tech.tablesaw.columns.instant.PackedInstant.weeksUntil;
-
-import java.time.LocalDateTime;
-import java.time.Month;
-import java.time.temporal.ChronoUnit;
-import java.time.temporal.UnsupportedTemporalTypeException;
-
 import com.google.common.base.Strings;
-
 import tech.tablesaw.api.DateColumn;
 import tech.tablesaw.api.DateTimeColumn;
 import tech.tablesaw.api.IntColumn;
@@ -52,6 +27,13 @@ import tech.tablesaw.columns.strings.StringColumnType;
 import tech.tablesaw.columns.temporal.TemporalMapFunctions;
 import tech.tablesaw.columns.times.TimeColumnType;
 
+import java.time.LocalDateTime;
+import java.time.Month;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.UnsupportedTemporalTypeException;
+
+import static tech.tablesaw.columns.datetimes.PackedLocalDateTime.*;
+
 public interface DateTimeMapFunctions extends TemporalMapFunctions<LocalDateTime> {
 
     default IntColumn hour() {
@@ -163,13 +145,61 @@ public interface DateTimeMapFunctions extends TemporalMapFunctions<LocalDateTime
             if (DateTimeColumn.valueIsMissing(c1)) {
                 newColumn.append(StringColumnType.missingValueIndicator());
             } else {
-                String yq = String.valueOf(getYear(c1)) + "-" + getQuarter(c1);
+                String yq = getYear(c1) + "-" + getQuarter(c1);
                 newColumn.append(yq);
             }
         }
         return newColumn;
     }
 
+    @Override
+    DateTimeColumn plus(long amountToAdd, ChronoUnit unit);
+
+    @Override
+    default DateTimeColumn plusYears(long amountToAdd) {
+        return plus(amountToAdd, ChronoUnit.YEARS);
+    }
+
+    @Override
+    default DateTimeColumn plusMonths(long amountToAdd) {
+        return plus(amountToAdd, ChronoUnit.MONTHS);
+    }
+
+    @Override
+    default DateTimeColumn plusWeeks(long amountToAdd) {
+        return plus(amountToAdd, ChronoUnit.WEEKS);
+    }
+
+    @Override
+    default DateTimeColumn plusDays(long amountToAdd) {
+        return plus(amountToAdd, ChronoUnit.DAYS);
+    }
+
+    @Override
+    default DateTimeColumn plusHours(long amountToAdd) {
+        return plus(amountToAdd, ChronoUnit.HOURS);
+    }
+
+    @Override
+    default DateTimeColumn plusMinutes(long amountToAdd) {
+        return plus(amountToAdd, ChronoUnit.MINUTES);
+    }
+
+    @Override
+    default DateTimeColumn plusSeconds(long amountToAdd) {
+        return plus(amountToAdd, ChronoUnit.SECONDS);
+    }
+
+    @Override
+    default DateTimeColumn plusMillis(long amountToAdd) {
+        return plus(amountToAdd, ChronoUnit.MILLIS);
+    }
+
+    @Override
+    default DateTimeColumn plusMicros(long amountToAdd) {
+        return plus(amountToAdd, ChronoUnit.MICROS);
+    }
+
     /**
      * Returns a StringColumn with the year and month from this column concatenated into a String that will sort
      * lexicographically in temporal order.
@@ -398,5 +428,4 @@ public interface DateTimeMapFunctions extends TemporalMapFunctions<LocalDateTime
     default LongColumn timeWindow(ChronoUnit unit, int n) {
         return timeWindow(unit, n, min());
     }
-
 }
diff --git a/core/src/main/java/tech/tablesaw/columns/instant/InstantMapFunctions.java b/core/src/main/java/tech/tablesaw/columns/instant/InstantMapFunctions.java
new file mode 100644
index 00000000..f0683873
--- /dev/null
+++ b/core/src/main/java/tech/tablesaw/columns/instant/InstantMapFunctions.java
@@ -0,0 +1,58 @@
+package tech.tablesaw.columns.instant;
+
+import tech.tablesaw.api.InstantColumn;
+import tech.tablesaw.columns.temporal.TemporalMapFunctions;
+
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+
+public interface InstantMapFunctions extends TemporalMapFunctions<Instant> {
+
+    @Override
+    InstantColumn plus(long amountToAdd, ChronoUnit unit);
+
+    @Override
+    default InstantColumn plusYears(long amountToAdd) {
+        return plus(amountToAdd, ChronoUnit.YEARS);
+    }
+
+    @Override
+    default InstantColumn plusMonths(long amountToAdd) {
+        return plus(amountToAdd, ChronoUnit.MONTHS);
+    }
+
+    @Override
+    default InstantColumn plusWeeks(long amountToAdd) {
+        return plus(amountToAdd, ChronoUnit.WEEKS);
+    }
+
+    @Override
+    default InstantColumn plusDays(long amountToAdd) {
+        return plus(amountToAdd, ChronoUnit.DAYS);
+    }
+
+    @Override
+    default InstantColumn plusHours(long amountToAdd) {
+        return plus(amountToAdd, ChronoUnit.HOURS);
+    }
+
+    @Override
+    default InstantColumn plusMinutes(long amountToAdd) {
+        return plus(amountToAdd, ChronoUnit.MINUTES);
+    }
+
+    @Override
+    default InstantColumn plusSeconds(long amountToAdd) {
+        return plus(amountToAdd, ChronoUnit.SECONDS);
+    }
+
+    @Override
+    default InstantColumn plusMillis(long amountToAdd) {
+        return plus(amountToAdd, ChronoUnit.MILLIS);
+    }
+
+    @Override
+    default InstantColumn plusMicros(long amountToAdd) {
+        return plus(amountToAdd, ChronoUnit.MICROS);
+    }
+}
diff --git a/core/src/main/java/tech/tablesaw/columns/numbers/NumberFillers.java b/core/src/main/java/tech/tablesaw/columns/numbers/NumberFillers.java
index 7ec44b31..66bc3dc6 100644
--- a/core/src/main/java/tech/tablesaw/columns/numbers/NumberFillers.java
+++ b/core/src/main/java/tech/tablesaw/columns/numbers/NumberFillers.java
@@ -26,4 +26,6 @@ public interface NumberFillers<T> {
     T fillWith(DoubleRangeIterable iterable);
 
     T fillWith(final DoubleSupplier supplier);
+
+    T fillWith(double d);
 }
diff --git a/core/src/main/java/tech/tablesaw/columns/numbers/NumberFilters.java b/core/src/main/java/tech/tablesaw/columns/numbers/NumberFilters.java
index 1f72f74c..e603b1dc 100644
--- a/core/src/main/java/tech/tablesaw/columns/numbers/NumberFilters.java
+++ b/core/src/main/java/tech/tablesaw/columns/numbers/NumberFilters.java
@@ -14,26 +14,15 @@
 
 package tech.tablesaw.columns.numbers;
 
-import static tech.tablesaw.columns.numbers.NumberPredicates.isBetweenExclusive;
-import static tech.tablesaw.columns.numbers.NumberPredicates.isBetweenInclusive;
-import static tech.tablesaw.columns.numbers.NumberPredicates.isEqualTo;
-import static tech.tablesaw.columns.numbers.NumberPredicates.isGreaterThan;
-import static tech.tablesaw.columns.numbers.NumberPredicates.isGreaterThanOrEqualTo;
-import static tech.tablesaw.columns.numbers.NumberPredicates.isLessThan;
-import static tech.tablesaw.columns.numbers.NumberPredicates.isLessThanOrEqualTo;
 import static tech.tablesaw.columns.numbers.NumberPredicates.isNegative;
 import static tech.tablesaw.columns.numbers.NumberPredicates.isNonNegative;
-import static tech.tablesaw.columns.numbers.NumberPredicates.isNotEqualTo;
 import static tech.tablesaw.columns.numbers.NumberPredicates.isPositive;
 import static tech.tablesaw.columns.numbers.NumberPredicates.isZero;
 
 import java.util.function.BiPredicate;
 import java.util.function.DoublePredicate;
 
-import tech.tablesaw.api.DoubleColumn;
 import tech.tablesaw.api.NumericColumn;
-import tech.tablesaw.filtering.predicates.DoubleBiPredicate;
-import tech.tablesaw.filtering.predicates.DoubleRangePredicate;
 import tech.tablesaw.selection.BitmapBackedSelection;
 import tech.tablesaw.selection.Selection;
 
@@ -41,45 +30,38 @@ public interface NumberFilters {
 
     Selection eval(DoublePredicate predicate);
 
-    Selection eval(DoubleRangePredicate predicate, Number rangeStart, Number rangeEnd);
-
-    Selection eval(DoubleBiPredicate predicate, DoubleColumn otherColumn);
-
-    Selection eval(DoubleBiPredicate predicate, Number value);
-
     Selection eval(BiPredicate<Number, Number> predicate, Number value);
 
-
     default Selection isEqualTo(double d) {
-        return eval(isEqualTo, d);
+        return eval(NumberPredicates.isEqualTo(d));
     }
 
     default Selection isNotEqualTo(double d) {
-        return eval(isNotEqualTo, d);
+        return eval(NumberPredicates.isNotEqualTo(d));
     }
 
-    default Selection isBetweenExclusive(double start, Number end) {
-        return eval(isBetweenExclusive, start, end);
+    default Selection isBetweenExclusive(double start, double end) {
+        return eval(NumberPredicates.isBetweenExclusive(start, end));
     }
 
-    default Selection isBetweenInclusive(double start, Number end) {
-        return eval(isBetweenInclusive, start, end);
+    default Selection isBetweenInclusive(double start, double end) {
+        return eval(NumberPredicates.isBetweenInclusive(start, end));
     }
 
     default Selection isGreaterThan(double f) {
-        return eval(isGreaterThan, f);
+        return eval(NumberPredicates.isGreaterThan(f));
     }
 
     default Selection isGreaterThanOrEqualTo(double f) {
-        return eval(isGreaterThanOrEqualTo, f);
+        return eval(NumberPredicates.isGreaterThanOrEqualTo(f));
     }
 
     default Selection isLessThan(double f) {
-        return eval(isLessThan, f);
+        return eval(NumberPredicates.isLessThan(f));
     }
 
     default Selection isLessThanOrEqualTo(double f) {
-        return eval(isLessThanOrEqualTo, f);
+        return eval(NumberPredicates.isLessThanOrEqualTo(f));
     }
 
     Selection isIn(Number... numbers);
diff --git a/core/src/main/java/tech/tablesaw/columns/numbers/NumberPredicates.java b/core/src/main/java/tech/tablesaw/columns/numbers/NumberPredicates.java
index 49a273f3..bfad2f1f 100644
--- a/core/src/main/java/tech/tablesaw/columns/numbers/NumberPredicates.java
+++ b/core/src/main/java/tech/tablesaw/columns/numbers/NumberPredicates.java
@@ -14,10 +14,6 @@
 
 package tech.tablesaw.columns.numbers;
 
-import tech.tablesaw.columns.Column;
-import tech.tablesaw.filtering.predicates.DoubleBiPredicate;
-import tech.tablesaw.filtering.predicates.DoubleRangePredicate;
-
 import java.util.function.DoublePredicate;
 
 /**
@@ -25,7 +21,7 @@ import java.util.function.DoublePredicate;
  * <p>
  * TODO(lwhite): Ensure each returns false when handling missing values
  */
-public interface NumberPredicates extends Column<Double> {
+public interface NumberPredicates {
 
     DoublePredicate isZero = i -> i == 0.0;
 
@@ -35,33 +31,37 @@ public interface NumberPredicates extends Column<Double> {
 
     DoublePredicate isNonNegative = i -> i >= 0;
 
-    DoubleBiPredicate isGreaterThan = (valueToTest, valueToCompareAgainst) -> valueToTest > valueToCompareAgainst;
+    static DoublePredicate isGreaterThan(double valueToCompareAgainst) {
+      return valueToTest -> valueToTest > valueToCompareAgainst;
+    }
 
-    DoubleRangePredicate isBetweenExclusive = new DoubleRangePredicate() {
-        @Override
-        public boolean test(double valueToTest, double rangeStart, double rangeEnd) {
-            return valueToTest > rangeStart && valueToTest < rangeEnd;
-        }
+    static DoublePredicate isBetweenExclusive(double rangeStart, double rangeEnd) {
+      return valueToTest -> valueToTest > rangeStart && valueToTest < rangeEnd;
     };
 
-    DoubleRangePredicate isBetweenInclusive = new DoubleRangePredicate() {
-        @Override
-        public boolean test(double valueToTest, double rangeStart, double rangeEnd) {
-            return valueToTest >= rangeStart && valueToTest <= rangeEnd;
-        }
+    static DoublePredicate isBetweenInclusive(double rangeStart, double rangeEnd) {
+      return valueToTest -> valueToTest >= rangeStart && valueToTest <= rangeEnd;
     };
 
-    DoubleBiPredicate isGreaterThanOrEqualTo = (valueToTest, valueToCompareAgainst) -> valueToTest >=
-            valueToCompareAgainst;
+    static DoublePredicate isGreaterThanOrEqualTo(double valueToCompareAgainst) {
+      return valueToTest -> valueToTest >= valueToCompareAgainst;  
+    }
 
-    DoubleBiPredicate isLessThan = (valueToTest, valueToCompareAgainst) -> valueToTest < valueToCompareAgainst;
+    static DoublePredicate isLessThan(double valueToCompareAgainst) {
+      return valueToTest -> valueToTest < valueToCompareAgainst;
+    }
 
-    DoubleBiPredicate isLessThanOrEqualTo = (valueToTest, valueToCompareAgainst) -> valueToTest <=
-            valueToCompareAgainst;
+    static DoublePredicate isLessThanOrEqualTo(double valueToCompareAgainst) {
+       return valueToTest -> valueToTest <= valueToCompareAgainst;
+    }
 
-    DoubleBiPredicate isEqualTo = (valueToTest, valueToCompareAgainst) -> valueToTest == valueToCompareAgainst;
+    static DoublePredicate isEqualTo(double valueToCompareAgainst) {
+      return valueToTest -> valueToTest == valueToCompareAgainst;
+    }
 
-    DoubleBiPredicate isNotEqualTo = (valueToTest, valueToCompareAgainst) -> valueToTest != valueToCompareAgainst;
+    static DoublePredicate isNotEqualTo(double valueToCompareAgainst) {
+      return valueToTest -> valueToTest != valueToCompareAgainst;
+    }
 
     DoublePredicate isMissing = i -> i != i;
 
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/columns/strings/StringMapFunctions.java b/core/src/main/java/tech/tablesaw/columns/strings/StringMapFunctions.java
index db86ac92..5577073b 100644
--- a/core/src/main/java/tech/tablesaw/columns/strings/StringMapFunctions.java
+++ b/core/src/main/java/tech/tablesaw/columns/strings/StringMapFunctions.java
@@ -18,6 +18,7 @@ import com.google.common.base.CharMatcher;
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
 import tech.tablesaw.api.DoubleColumn;
+import tech.tablesaw.api.FloatColumn;
 import tech.tablesaw.api.IntColumn;
 import tech.tablesaw.api.StringColumn;
 import tech.tablesaw.columns.Column;
@@ -145,11 +146,56 @@ public interface StringMapFunctions extends Column<String> {
         return newColumn;
     }
 
+    /**
+     * Returns an IntColumn containing all the values of this string column as integers,
+     * assuming all the values are stringified ints in the first place. Otherwise an exception is thrown
+     *
+     * @return  An IntColumn containing ints parsed from the strings in this column
+     */
     default IntColumn parseInt() {
-
         IntColumn newColumn = IntColumn.create(name() + "[parsed]");
-        for (int r = 0; r < size(); r++) {
-            newColumn.append(Integer.parseInt(getString(r)));
+        for (String s : this) {
+            if (StringColumn.valueIsMissing(s)) {
+                newColumn.appendMissing();
+            } else {
+                newColumn.append(Integer.parseInt(s));
+            }
+        }
+        return newColumn;
+    }
+
+    /**
+     * Returns an Double containing all the values of this string column as doubles,
+     * assuming all the values are stringified doubles in the first place. Otherwise an exception is thrown
+     *
+     * @return  A DoubleColumn containing doubles parsed from the strings in this column
+     */
+    default DoubleColumn parseDouble() {
+        DoubleColumn newColumn = DoubleColumn.create(name() + "[parsed]");
+        for (String s : this) {
+            if (StringColumn.valueIsMissing(s)) {
+                newColumn.appendMissing();
+            } else {
+                newColumn.append(Double.parseDouble(s));
+            }
+        }
+        return newColumn;
+    }
+
+    /**
+     * Returns an Float containing all the values of this string column as floats,
+     * assuming all the values are stringified floats in the first place. Otherwise an exception is thrown
+     *
+     * @return  A FloatColumn containing floats parsed from the strings in this column
+     */
+    default FloatColumn parseFloat() {
+        FloatColumn newColumn = FloatColumn.create(name() + "[parsed]");
+        for (String s : this) {
+            if (StringColumn.valueIsMissing(s)) {
+                newColumn.appendMissing();
+            } else {
+                newColumn.append(Float.parseFloat(s));
+            }
         }
         return newColumn;
     }
diff --git a/core/src/main/java/tech/tablesaw/columns/temporal/TemporalMapFunctions.java b/core/src/main/java/tech/tablesaw/columns/temporal/TemporalMapFunctions.java
index 9cdfef4b..be23c55d 100644
--- a/core/src/main/java/tech/tablesaw/columns/temporal/TemporalMapFunctions.java
+++ b/core/src/main/java/tech/tablesaw/columns/temporal/TemporalMapFunctions.java
@@ -14,19 +14,17 @@
 
 package tech.tablesaw.columns.temporal;
 
-import static tech.tablesaw.columns.datetimes.PackedLocalDateTime.asLocalDateTime;
+import tech.tablesaw.api.BooleanColumn;
+import tech.tablesaw.api.LongColumn;
+import tech.tablesaw.columns.Column;
+import tech.tablesaw.columns.booleans.BooleanColumnType;
 
 import java.time.LocalDateTime;
 import java.time.temporal.ChronoUnit;
 import java.time.temporal.Temporal;
 import java.time.temporal.TemporalUnit;
 
-import tech.tablesaw.api.BooleanColumn;
-import tech.tablesaw.api.LongColumn;
-import tech.tablesaw.columns.Column;
-import tech.tablesaw.columns.booleans.BooleanColumnType;
-import tech.tablesaw.columns.instant.InstantColumnType;
-import tech.tablesaw.columns.instant.PackedInstant;
+import static tech.tablesaw.columns.datetimes.PackedLocalDateTime.asLocalDateTime;
 
 public interface TemporalMapFunctions<T extends Temporal> extends TemporalColumn<T> {
 
@@ -80,21 +78,7 @@ public interface TemporalMapFunctions<T extends Temporal> extends TemporalColumn
         return newColumn;
     }
     
-    default Column<T> plus(long amountToAdd, ChronoUnit unit) {
-        TemporalColumn<T> newColumn = emptyCopy();
-        newColumn.setName(temporalColumnName(this, amountToAdd, unit));
-        TemporalColumn<T> column1 = this;
-
-        for (int r = 0; r < column1.size(); r++) {
-            long packedDateTime = column1.getLongInternal(r);
-            if (packedDateTime == InstantColumnType.missingValueIndicator()) {
-                newColumn.appendMissing();
-            } else {
-                newColumn.appendInternal(PackedInstant.plus(packedDateTime, amountToAdd, unit));
-            }
-        }
-        return newColumn;
-    }
+    Column<T> plus(long amountToAdd, ChronoUnit unit);
 
     default Column<T> plusYears(long amountToAdd) {
         return plus(amountToAdd, ChronoUnit.YEARS);
diff --git a/core/src/main/java/tech/tablesaw/conversion/smile/SmileConverter.java b/core/src/main/java/tech/tablesaw/conversion/smile/SmileConverter.java
index 6132101f..8246ed38 100644
--- a/core/src/main/java/tech/tablesaw/conversion/smile/SmileConverter.java
+++ b/core/src/main/java/tech/tablesaw/conversion/smile/SmileConverter.java
@@ -110,8 +110,9 @@ public class SmileConverter {
     }
 
     private NominalAttribute colAsNominalAttribute(Column<?> col) {
+        Column<?> unique = col.unique();
         return new NominalAttribute(col.name(),
-            col.unique().mapInto(o -> o.toString(), StringColumn.create(col.name(), col.size())).asObjectArray());
+            unique.mapInto(o -> o.toString(), StringColumn.create(col.name(), unique.size())).asObjectArray());
     }
 
     private static enum AttributeType {
diff --git a/core/src/main/java/tech/tablesaw/filtering/predicates/DoubleBiPredicate.java b/core/src/main/java/tech/tablesaw/filtering/predicates/DoubleBiPredicate.java
deleted file mode 100644
index 8f4aa2ea..00000000
--- a/core/src/main/java/tech/tablesaw/filtering/predicates/DoubleBiPredicate.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * 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.filtering.predicates;
-
-public interface DoubleBiPredicate {
-
-    /**
-     * Returns true if valueToTest meets the criteria of this predicate when valueToCompareAgainst is considered
-     * <p>
-     * Example (to compare all the values v in a column such that v is greater than 4.0, v is the value to test and 4.0
-     * is the value to compare against
-     *
-     * @param valueToTest           the value you're checking. Often this is the value of a cell in a double column
-     * @param valueToCompareAgainst the value to compare against. Often this is a single value for all comparisons
-     */
-    boolean test(double valueToTest, double valueToCompareAgainst);
-}
diff --git a/core/src/main/java/tech/tablesaw/filtering/predicates/DoubleRangePredicate.java b/core/src/main/java/tech/tablesaw/filtering/predicates/DoubleRangePredicate.java
deleted file mode 100644
index 091c4e30..00000000
--- a/core/src/main/java/tech/tablesaw/filtering/predicates/DoubleRangePredicate.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * 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.filtering.predicates;
-
-public interface DoubleRangePredicate {
-
-    /**
-     * Returns true if valueToTest meets the criteria of this predicate when rangeStart is considered
-     * <p>
-     * Example (to compare all the values v in a column such that v is greater than 4.0 and less than 6.0.
-     *
-     * @param valueToTest the value you're checking. Often this is the value of a cell in a double column
-     * @param rangeStart  the start of the range to compare against. Often this is a single value for all comparisons
-     * @param rangeEnd    the end of the range to compare against. Often this is a single value for all comparisons
-     */
-    boolean test(double valueToTest, double rangeStart, double rangeEnd);
-}
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/core/src/main/java/tech/tablesaw/io/Source.java b/core/src/main/java/tech/tablesaw/io/Source.java
index 7b1ba4b9..57f2e052 100644
--- a/core/src/main/java/tech/tablesaw/io/Source.java
+++ b/core/src/main/java/tech/tablesaw/io/Source.java
@@ -15,16 +15,14 @@ import java.util.Scanner;
 
 public class Source {
 
+    // we always have one of these (file, reader, or inputStream)
     protected final File file;
     protected final Reader reader;
     protected final InputStream inputStream;
     protected final Charset charset;
 
     public Source(File file) {
-        this.file = file;
-        this.reader = null;
-        this.inputStream = null;
-        this.charset = Charset.defaultCharset();
+        this(file, Charset.defaultCharset());
     }
 
     public Source(File file, Charset charset) {
@@ -42,10 +40,7 @@ public class Source {
     }
 
     public Source(InputStream inputStream) {
-        this.file = null;
-        this.reader = null;
-        this.inputStream = inputStream;
-        this.charset = Charset.defaultCharset();
+        this(inputStream, Charset.defaultCharset());
     }
 
     public Source(InputStream inputStream, Charset charset) {
diff --git a/core/src/main/java/tech/tablesaw/io/csv/CsvReadOptions.java b/core/src/main/java/tech/tablesaw/io/csv/CsvReadOptions.java
index 9f25dca0..3348c6a3 100644
--- a/core/src/main/java/tech/tablesaw/io/csv/CsvReadOptions.java
+++ b/core/src/main/java/tech/tablesaw/io/csv/CsvReadOptions.java
@@ -31,6 +31,7 @@ public class CsvReadOptions extends ReadOptions {
 
     private final ColumnType[] columnTypes;
     private final Character separator;
+    private final Character quoteChar;
     private final String lineEnding;
     private final Integer maxNumberOfColumns;
     private final Character commentPrefix;
@@ -41,6 +42,7 @@ public class CsvReadOptions extends ReadOptions {
 
         columnTypes = builder.columnTypes;
         separator = builder.separator;
+        quoteChar = builder.quoteChar;
         lineEnding = builder.lineEnding;
         maxNumberOfColumns = builder.maxNumberOfColumns;
         commentPrefix = builder.commentPrefix;
@@ -108,6 +110,10 @@ public class CsvReadOptions extends ReadOptions {
         return separator;
     }
 
+    public Character quoteChar() {
+        return quoteChar;
+    }
+
     public String lineEnding() {
         return lineEnding;
     }
@@ -131,6 +137,7 @@ public class CsvReadOptions extends ReadOptions {
     public static class Builder extends ReadOptions.Builder {
 
         private Character separator = ',';
+        private Character quoteChar;
         private String lineEnding;
         private ColumnType[] columnTypes;
         private Integer maxNumberOfColumns = 10_000;
@@ -167,6 +174,11 @@ public class CsvReadOptions extends ReadOptions {
             return this;
         }
 
+        public Builder quoteChar(Character quoteChar) {
+            this.quoteChar = quoteChar;
+            return this;
+        }
+
         public Builder lineEnding(String lineEnding) {
             this.lineEnding = lineEnding;
             this.lineSeparatorDetectionEnabled = false;
diff --git a/core/src/main/java/tech/tablesaw/io/csv/CsvReader.java b/core/src/main/java/tech/tablesaw/io/csv/CsvReader.java
index 0c080b11..0b06fafe 100644
--- a/core/src/main/java/tech/tablesaw/io/csv/CsvReader.java
+++ b/core/src/main/java/tech/tablesaw/io/csv/CsvReader.java
@@ -159,6 +159,9 @@ public class CsvReader extends FileReader implements DataReader<CsvReadOptions>
 
     private CsvFormat csvFormat(CsvReadOptions options) {
         CsvFormat format = new CsvFormat();
+        if (options.quoteChar() != null) {
+            format.setQuote(options.quoteChar());
+        }
         if (options.separator() != null) {
             format.setDelimiter(options.separator());
         }
diff --git a/core/src/main/java/tech/tablesaw/io/csv/CsvWriteOptions.java b/core/src/main/java/tech/tablesaw/io/csv/CsvWriteOptions.java
index e088052d..b3188829 100644
--- a/core/src/main/java/tech/tablesaw/io/csv/CsvWriteOptions.java
+++ b/core/src/main/java/tech/tablesaw/io/csv/CsvWriteOptions.java
@@ -13,16 +13,16 @@ public class CsvWriteOptions extends WriteOptions {
 
     private final boolean header;
     private final char separator;
-    private final char quotechar;
-    private final char escapechar;
+    private final char quoteChar;
+    private final char escapeChar;
     private final String lineEnd;
 
     private CsvWriteOptions(Builder builder) {
         super(builder);
         this.header = builder.header;
         this.separator = builder.separator;
-        this.quotechar = builder.quoteChar;
-        this.escapechar = builder.escapeChar;
+        this.quoteChar = builder.quoteChar;
+        this.escapeChar = builder.escapeChar;
         this.lineEnd = builder.lineEnd;
     }
 
@@ -35,11 +35,11 @@ public class CsvWriteOptions extends WriteOptions {
     }
 
     public char escapeChar() {
-        return escapechar;
+        return escapeChar;
     }
 
     public char quoteChar() {
-        return quotechar;
+        return quoteChar;
     }
 
     public String lineEnd() {
diff --git a/core/src/main/java/tech/tablesaw/io/fixed/FixedWidthReadOptions.java b/core/src/main/java/tech/tablesaw/io/fixed/FixedWidthReadOptions.java
index c0a544f5..86cd426b 100644
--- a/core/src/main/java/tech/tablesaw/io/fixed/FixedWidthReadOptions.java
+++ b/core/src/main/java/tech/tablesaw/io/fixed/FixedWidthReadOptions.java
@@ -29,7 +29,6 @@ import java.util.Locale;
 
 public class FixedWidthReadOptions extends ReadOptions {
 
-    // we always have one of these (file, reader, or inputStream)
     private final ColumnType[] columnTypes;
     private final FixedWidthFields columnSpecs;
     private final String lineEnding;
@@ -174,6 +173,10 @@ public class FixedWidthReadOptions extends ReadOptions {
             return this;
         }
 
+        public Builder systemLineEnding() {
+            return lineEnding(System.lineSeparator());
+        }
+
         public Builder padding(char padding) {
             this.padding = padding;
             return this;
diff --git a/core/src/main/java/tech/tablesaw/io/string/DataFramePrinter.java b/core/src/main/java/tech/tablesaw/io/string/DataFramePrinter.java
index 02fd5b6c..2c765de4 100644
--- a/core/src/main/java/tech/tablesaw/io/string/DataFramePrinter.java
+++ b/core/src/main/java/tech/tablesaw/io/string/DataFramePrinter.java
@@ -14,13 +14,14 @@
 
 package tech.tablesaw.io.string;
 
-import tech.tablesaw.table.Relation;
-import tech.tablesaw.util.StringUtils;
-
 import java.io.IOException;
 import java.io.OutputStream;
 import java.util.stream.IntStream;
 
+import tech.tablesaw.columns.Column;
+import tech.tablesaw.table.Relation;
+import tech.tablesaw.util.StringUtils;
+
 /**
  * A class that can pretty print a DataFrame to text for visualization in a console
  * <p>
@@ -29,6 +30,8 @@ import java.util.stream.IntStream;
  */
 public class DataFramePrinter {
 
+    private final static String TOO_SHORT_COLUMN_MARKER = "?"; 
+
     private final int maxRows;
     private final OutputStream stream;
 
@@ -172,6 +175,10 @@ public class DataFramePrinter {
         return header;
     }
 
+    private String getDataToken(Column<?> col, int i) {
+        return col.size() > i ? col.getString(i) : TOO_SHORT_COLUMN_MARKER;
+    }
+
     /**
      * Returns the 2-D array of data tokens from the frame specified
      *
@@ -189,7 +196,8 @@ public class DataFramePrinter {
             int i;
             for (i = 0; i < Math.ceil((double) rowCount / 2); i++) {
                 for (int j = 0; j < colCount; j++) {
-                    data[i][j] = frame.getString(i, j);
+                    Column<?> col = frame.column(j);
+                    data[i][j] = getDataToken(col, i);
                 }
             }
             for (int j = 0; j < colCount; j++) {
@@ -197,20 +205,20 @@ public class DataFramePrinter {
             }
             for (++i; i <= rowCount; i++) {
                 for (int j = 0; j < colCount; j++) {
-                    data[i][j] = frame.getString(frame.rowCount() - maxRows + i - 1, j);
+                    Column<?> col = frame.column(j);
+                    data[i][j] = getDataToken(col, frame.rowCount() - maxRows + i - 1);
                 }
             }
         } else {
             data = new String[rowCount][colCount];
             for (int i = 0; i < rowCount; i++) {
                 for (int j = 0; j < colCount; j++) {
-                    String value = frame.getString(i, j);
+                    Column<?> col = frame.column(j);
+                    String value = getDataToken(col, i);
                     data[i][j] = value == null ? "" : value;
                 }
             }
         }
         return data;
     }
-
 }
-
diff --git a/core/src/main/java/tech/tablesaw/joining/DataFrameJoiner.java b/core/src/main/java/tech/tablesaw/joining/DataFrameJoiner.java
index 2111c6c6..bf059ef9 100644
--- a/core/src/main/java/tech/tablesaw/joining/DataFrameJoiner.java
+++ b/core/src/main/java/tech/tablesaw/joining/DataFrameJoiner.java
@@ -276,11 +276,10 @@ public class DataFrameJoiner {
             }
 
             Table table2Rows = table2.where(rowBitMapMultiCol);
-            table2Rows.removeColumns(col2Names);
             if (outer && table2Rows.isEmpty()) {
                 withMissingLeftJoin(result, table1Rows);
             } else {
-                crossProduct(result, table1Rows, table2Rows);
+                crossProduct(result, table1Rows, table2Rows, col2Names);
             }
         }
         return result;
@@ -649,10 +648,13 @@ public class DataFrameJoiner {
     }
 
     @SuppressWarnings({"rawtypes", "unchecked"})
-    private void crossProduct(Table destination, Table table1, Table table2) {
+    private void crossProduct(Table destination, Table table1, Table table2, String[] col2Names) {
+        int table2RowCount = table2.rowCount();
+        // Remove join columns
+        table2.removeColumns(col2Names);
         for (int c = 0; c < table1.columnCount() + table2.columnCount(); c++) {
             for (int r1 = 0; r1 < table1.rowCount(); r1++) {
-                for (int r2 = 0; r2 < table2.rowCount(); r2++) {
+                for (int r2 = 0; r2 < table2RowCount; r2++) {
                     if (c < table1.columnCount()) {
                         Column t1Col = table1.column(c);
                         destination.column(c).append(t1Col, r1);
diff --git a/core/src/main/java/tech/tablesaw/table/TableSliceGroup.java b/core/src/main/java/tech/tablesaw/table/TableSliceGroup.java
index 422ca410..537e0e23 100644
--- a/core/src/main/java/tech/tablesaw/table/TableSliceGroup.java
+++ b/core/src/main/java/tech/tablesaw/table/TableSliceGroup.java
@@ -153,16 +153,16 @@ public class TableSliceGroup implements Iterable<TableSlice> {
         Table groupTable = summaryTableName(sourceTable);
         StringColumn groupColumn = StringColumn.create("Group");
         groupTable.addColumns(groupColumn);
+        boolean firstFunction = true;
         for (Map.Entry<String, Collection<AggregateFunction<?,?>>> entry : functions.asMap().entrySet()) {
             String columnName = entry.getKey();
-            int functionCount = 0;
             for (AggregateFunction function : entry.getValue()) {
                 String colName = aggregateColumnName(columnName, function.functionName());
                 ColumnType type = function.returnType();
                 Column resultColumn = type.create(colName);
                 for (TableSlice subTable : getSlices()) {
                     Object result = function.summarize(subTable.column(columnName));
-                    if (functionCount == 0) {
+                    if (firstFunction) {
                         groupColumn.append(subTable.name());
                     }
                     if (result instanceof Number) {
@@ -173,7 +173,7 @@ public class TableSliceGroup implements Iterable<TableSlice> {
                     }
                 }
                 groupTable.addColumns(resultColumn);
-                functionCount++;
+                firstFunction = false;
             }
         }
         return splitGroupingColumn(groupTable);
diff --git a/excel/pom.xml b/excel/pom.xml
index 536279dc..07a12868 100644
--- a/excel/pom.xml
+++ b/excel/pom.xml
@@ -5,7 +5,7 @@
   <parent>
     <groupId>tech.tablesaw</groupId>
     <artifactId>tablesaw-parent</artifactId>
-    <version>0.34.1-SNAPSHOT</version>
+    <version>0.34.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>tablesaw-excel</artifactId>
@@ -75,7 +75,7 @@
     <dependency>
       <groupId>tech.tablesaw</groupId>
       <artifactId>tablesaw-core</artifactId>
-      <version>0.34.1-SNAPSHOT</version>
+      <version>0.34.2-SNAPSHOT</version>
       <scope>provided</scope>
     </dependency>
     <dependency>
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/pom.xml b/html/pom.xml
index 5f22d96f..703e3bcf 100644
--- a/html/pom.xml
+++ b/html/pom.xml
@@ -5,7 +5,7 @@
   <parent>
     <groupId>tech.tablesaw</groupId>
     <artifactId>tablesaw-parent</artifactId>
-    <version>0.34.1-SNAPSHOT</version>
+    <version>0.34.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>tablesaw-html</artifactId>
@@ -75,7 +75,7 @@
     <dependency>
       <groupId>tech.tablesaw</groupId>
       <artifactId>tablesaw-core</artifactId>
-      <version>0.34.1-SNAPSHOT</version>
+      <version>0.34.2-SNAPSHOT</version>
       <scope>provided</scope>
     </dependency>
     <dependency>
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/pom.xml b/json/pom.xml
index 73b3dcbc..1309add0 100644
--- a/json/pom.xml
+++ b/json/pom.xml
@@ -5,7 +5,7 @@
   <parent>
     <groupId>tech.tablesaw</groupId>
     <artifactId>tablesaw-parent</artifactId>
-    <version>0.34.1-SNAPSHOT</version>
+    <version>0.34.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>tablesaw-json</artifactId>
@@ -75,13 +75,13 @@
     <dependency>
       <groupId>tech.tablesaw</groupId>
       <artifactId>tablesaw-core</artifactId>
-      <version>0.34.1-SNAPSHOT</version>
+      <version>0.34.2-SNAPSHOT</version>
       <scope>provided</scope>
     </dependency>
     <dependency>
       <groupId>com.fasterxml.jackson.core</groupId>
       <artifactId>jackson-databind</artifactId>
-      <version>2.9.9</version>
+      <version>2.9.9.2</version>
     </dependency>
     <dependency>
       <groupId>com.github.wnameless</groupId>
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/pom.xml b/jsplot/pom.xml
index cfa934d9..2e68d7af 100644
--- a/jsplot/pom.xml
+++ b/jsplot/pom.xml
@@ -5,7 +5,7 @@
     <parent>
         <groupId>tech.tablesaw</groupId>
         <artifactId>tablesaw-parent</artifactId>
-        <version>0.34.1-SNAPSHOT</version>
+        <version>0.34.2-SNAPSHOT</version>
     </parent>
 
     <artifactId>tablesaw-jsplot</artifactId>
@@ -85,7 +85,7 @@
         <dependency>
             <groupId>tech.tablesaw</groupId>
             <artifactId>tablesaw-core</artifactId>
-            <version>0.34.1-SNAPSHOT</version>
+            <version>0.34.2-SNAPSHOT</version>
         </dependency>
         <dependency>
             <groupId>org.junit.jupiter</groupId>
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/Utils.java b/jsplot/src/main/java/tech/tablesaw/plotly/Utils.java
index e076a51d..bd8cb83b 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/Utils.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/Utils.java
@@ -19,7 +19,7 @@ public class Utils {
         for (int i = 0; i < data.length; i++) {
             Object o = data[i];
             builder.append("'");
-            builder.append(String.valueOf(o));
+            builder.append(o);
             builder.append("'");
             if (i < data.length - 1) {
                 builder.append(",");
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/api/QQPlot.java b/jsplot/src/main/java/tech/tablesaw/plotly/api/QQPlot.java
new file mode 100644
index 00000000..aae3b1f8
--- /dev/null
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/api/QQPlot.java
@@ -0,0 +1,102 @@
+package tech.tablesaw.plotly.api;
+
+import com.google.common.base.Preconditions;
+import org.apache.commons.math3.stat.StatUtils;
+import tech.tablesaw.api.NumberColumn;
+import tech.tablesaw.api.Table;
+import tech.tablesaw.plotly.components.Axis;
+import tech.tablesaw.plotly.components.Figure;
+import tech.tablesaw.plotly.components.Layout;
+import tech.tablesaw.plotly.traces.ScatterTrace;
+
+import java.util.Arrays;
+
+public class QQPlot {
+
+    /**
+     * Returns a figure containing a QQ Plot describing the differences between the distribution of values
+     * in the columns of interest
+     * @param title         A title for the plot
+     * @param table         The table containing the columns of interest
+     * @param columnName1   The name of the first numeric column containing the data to plot
+     * @param columnName2   The name of the second numeric column containing the data to plot
+     * @return              A quantile plot
+     */
+    public static Figure create(String title, Table table, String columnName1, String columnName2) {
+
+        NumberColumn<?> xCol = table.nCol(columnName1);
+        NumberColumn<?> yCol = table.nCol(columnName2);
+
+        return create(title, xCol.name(), xCol.asDoubleArray(), yCol.name(), yCol.asDoubleArray());
+    }
+
+    /**
+     * Returns a figure containing a QQ Plot describing the differences between the distribution of values
+     * in the columns of interest
+     * @param title    A title for the plot
+     * @param xTitle   The name of the first numeric column containing the data to plot
+     * @param xData    The data to plot on the x Axis
+     * @param yTitle   The name of the second numeric column containing the data to plot
+     * @param yData    The data to plot on the y Axis
+     * @return         A quantile plot
+     */
+    public static Figure create(String title, String xTitle, double[] xData, String yTitle, double[] yData) {
+
+        Preconditions.checkArgument(xData.length != 0, "x Data array is empty");
+        Preconditions.checkArgument(yData.length != 0, "x Data array is empty");
+
+        if (xData.length != yData.length) {
+            double[] interpolatedData;
+            if (xData.length < yData.length) {
+                interpolatedData = interpolate(yData, xData.length);
+                yData = interpolatedData;
+            } else {
+                interpolatedData = interpolate(xData, yData.length);
+                xData = interpolatedData;
+            }
+        }
+
+        Arrays.sort(xData);
+        Arrays.sort(yData);
+        double min = Math.min(xData[0], yData[0]);
+        double max = Math.max(xData[xData.length - 1], yData[yData.length - 1]);
+        double[] line = {min, max};
+
+        // Draw the 45 degree line indicating equal distributions
+        ScatterTrace trace1 = ScatterTrace.builder(line, line)
+                .mode(ScatterTrace.Mode.LINE)
+                .name("y = x")
+                .build();
+
+        // Draw the actual data points
+        ScatterTrace trace2 = ScatterTrace.builder(xData, yData)
+                .name("distributions")
+                .build();
+
+        Layout layout = Layout.builder()
+                .title(title)
+                .xAxis(Axis.builder()
+                        .title(xTitle)
+                        .build())
+                .yAxis(Axis.builder().title(yTitle).build())
+                .height(700)
+                .width(900)
+                .build();
+        return new Figure(layout, trace1, trace2);
+    }
+
+    /**
+     * Returns a double array, whose values are quantiles from the given source, based on the given size.
+     * The idea is to produce size elements that represent the quantiles of source array
+     * @param source    The array to whose quantiles are calculated
+     * @param size      The size of the array to return
+     */
+    private static double[] interpolate(double[] source, int size) {
+        double[] interpolatedData = new double[size];
+        for (int i = 0; i < size; i++) {
+            double value = ((i + .5) / (double) size) * 100;
+            interpolatedData[i] = StatUtils.percentile(source, value);
+        }
+        return interpolatedData;
+    }
+}
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/api/QuantilePlot.java b/jsplot/src/main/java/tech/tablesaw/plotly/api/QuantilePlot.java
new file mode 100644
index 00000000..70a970d9
--- /dev/null
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/api/QuantilePlot.java
@@ -0,0 +1,35 @@
+package tech.tablesaw.plotly.api;
+
+import tech.tablesaw.api.NumberColumn;
+import tech.tablesaw.api.Table;
+import tech.tablesaw.plotly.components.Figure;
+import tech.tablesaw.plotly.components.Layout;
+import tech.tablesaw.plotly.traces.ScatterTrace;
+
+public class QuantilePlot {
+
+    /**
+     * Returns a figure containing a Quantile Plot describing the distribution of values in the column of interest
+     * @param title         A title for the plot
+     * @param table         The table containing the column of interest
+     * @param columnName    The name of the numeric column containing the data to plot
+     * @return              A quantile plot
+     */
+    public static Figure create(String title, Table table, String columnName) {
+
+        NumberColumn<?> xCol = table.nCol(columnName);
+
+        double[] x = new double[xCol.size()];
+
+        for (int i = 0; i < x.length; i++) {
+            x[i] = i / (float) x.length;
+        }
+
+        NumberColumn<?> copy = xCol.copy();
+        copy.sortAscending();
+
+        ScatterTrace trace = ScatterTrace.builder(x, copy.asDoubleArray()).build();
+        Layout layout = Layout.builder().title(title).build();
+        return new Figure(layout, trace);
+    }
+}
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/api/TimeSeriesPlot.java b/jsplot/src/main/java/tech/tablesaw/plotly/api/TimeSeriesPlot.java
index fc9fb889..9921a181 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/api/TimeSeriesPlot.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/api/TimeSeriesPlot.java
@@ -1,6 +1,7 @@
 package tech.tablesaw.plotly.api;
 
 import tech.tablesaw.api.DateColumn;
+import tech.tablesaw.api.DateTimeColumn;
 import tech.tablesaw.api.NumberColumn;
 import tech.tablesaw.api.Table;
 import tech.tablesaw.plotly.components.Figure;
@@ -51,4 +52,32 @@ public class TimeSeriesPlot {
         return new Figure(layout, trace);
     }
 
+    public static Figure create(String title, String xTitle, DateTimeColumn xCol, String yTitle, NumberColumn<?> yCol) {
+        Layout layout = Layout.builder(title, xTitle, yTitle).build();
+        ScatterTrace trace = ScatterTrace.builder(xCol, yCol)
+                .mode(ScatterTrace.Mode.LINE)
+                .build();
+        return new Figure(layout, trace);
+    }
+
+    /**
+     * Creates a time series where the x values are from a DateTimeColumn, rather than a DateColumn
+     * @param title                 The title of the plot
+     * @param table                 The table containing the source data
+     * @param dateTimeColumnName    The name of a DateTimeColumn
+     * @param numberColumnName      The name of a NumberColumn
+     * @return                      The figure to be displayed
+     */
+    public static Figure createDateTimeSeries(String title, Table table, String dateTimeColumnName, String numberColumnName) {
+
+        DateTimeColumn xCol = table.dateTimeColumn(dateTimeColumnName);
+        NumberColumn<?> yCol = table.numberColumn(numberColumnName);
+
+        Layout layout = Layout.builder(title, xCol.name(), yCol.name()).build();
+
+        ScatterTrace trace = ScatterTrace.builder(xCol, yCol)
+                .mode(ScatterTrace.Mode.LINE)
+                .build();
+        return new Figure(layout, trace);
+    }
 }
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/api/TukeyMeanDifferencePlot.java b/jsplot/src/main/java/tech/tablesaw/plotly/api/TukeyMeanDifferencePlot.java
new file mode 100644
index 00000000..4acd0c90
--- /dev/null
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/api/TukeyMeanDifferencePlot.java
@@ -0,0 +1,121 @@
+package tech.tablesaw.plotly.api;
+
+import com.google.common.base.Preconditions;
+import org.apache.commons.math3.stat.StatUtils;
+import tech.tablesaw.api.NumberColumn;
+import tech.tablesaw.api.Table;
+import tech.tablesaw.plotly.components.Axis;
+import tech.tablesaw.plotly.components.Figure;
+import tech.tablesaw.plotly.components.Layout;
+import tech.tablesaw.plotly.traces.ScatterTrace;
+
+import java.util.Arrays;
+
+/**
+ *
+ * A Tukey Mean-Difference Plot (AKA a Bland-Altman plot) is a kind of scatter plot used frequently in medicine,
+ * biology and other fields, is used to visualize the differences between two quantitative measurements. In particular,
+ * it is often used to evaluate whether two tests produce 'the same' result.
+ *
+ * For two numeric arrays, a and b, the plot shows the mean for each pair of observations (a + b) / 2, as well as the
+ * differences between them: a - b.
+ *
+ * For more information:
+ * https://en.wikipedia.org/wiki/Bland%E2%80%93Altman_plot
+ */
+public class TukeyMeanDifferencePlot {
+
+    /**
+     * Returns a figure containing a Tukey Mean-Difference Plot describing the differences between the data in two
+     * columns of interest
+     * @param title         A title for the plot
+     * @param measure       The measure being compared on the plot (e.g "inches" or "height in inches"
+     * @param table         The table containing the columns of interest
+     * @param columnName1   The name of the first numeric column containing the data to plot
+     * @param columnName2   The name of the second numeric column containing the data to plot
+     * @return              A quantile plot
+     */
+    public static Figure create(String title, String measure, Table table, String columnName1, String columnName2) {
+
+        NumberColumn<?> xCol = table.nCol(columnName1);
+        NumberColumn<?> yCol = table.nCol(columnName2);
+
+        return create(title, measure, xCol.asDoubleArray(), yCol.asDoubleArray());
+    }
+
+    /**
+     * Returns a figure containing a QQ Plot describing the differences between the distribution of values
+     * in the columns of interest
+     * @param title    A title for the plot
+     * @param measure  The measure being compared on the plot (e.g "inches" or "height in inches"
+     * @param xData    The data to plot on the x Axis
+     * @param yData    The data to plot on the y Axis
+     * @return         A quantile plot
+     */
+    public static Figure create(String title, String measure, double[] xData, double[] yData) {
+
+        Preconditions.checkArgument(xData.length != 0, "x Data array is empty");
+        Preconditions.checkArgument(yData.length != 0, "x Data array is empty");
+
+        if (xData.length != yData.length) {
+            double[] interpolatedData;
+            if (xData.length < yData.length) {
+                interpolatedData = interpolate(yData, xData.length);
+                yData = interpolatedData;
+            } else {
+                interpolatedData = interpolate(xData, yData.length);
+                xData = interpolatedData;
+            }
+        }
+
+        Arrays.sort(xData);
+        Arrays.sort(yData);
+
+        double[] averagePoints = new double[xData.length];
+        double[] differencePoints = new double[xData.length];
+        for (int i = 0; i < xData.length; i++) {
+            averagePoints[i] = (xData[i] + yData[i]) /2.0;
+            differencePoints[i] = (xData[i] - yData[i]);
+        }
+
+        double xMin = StatUtils.min(xData);
+        double xMax = StatUtils.max(xData);
+        double[] zeroLineX = {xMin, xMax};
+        double[] zeroLineY = {0, 0};
+
+        // Draw the line indicating equal distributions (this is zero in this plot)
+        ScatterTrace trace1 = ScatterTrace.builder(zeroLineX, zeroLineY)
+                .mode(ScatterTrace.Mode.LINE)
+                .name("y = x")
+                .build();
+
+        // Draw the actual data points
+        ScatterTrace trace2 = ScatterTrace.builder(averagePoints, differencePoints)
+                .name("mean x difference")
+                .build();
+
+        Layout layout = Layout.builder()
+                .title(title)
+                .xAxis(Axis.builder().title("mean (" + measure + ")").build())
+                .yAxis(Axis.builder().title("difference (" + measure + ")").build())
+                .height(700)
+                .width(900)
+                .build();
+        return new Figure(layout, trace1, trace2);
+    }
+
+    /**
+     * Returns a double array, whose values are quantiles from the given source, based on the given size.
+     * The idea is to produce size elements that represent the quantiles of source array
+     * @param source    The array to whose quantiles are calculated
+     * @param size      The size of the array to return
+     */
+    private static double[] interpolate(double[] source, int size) {
+        double[] interpolatedData = new double[size];
+        for (int i = 0; i < size; i++) {
+            double value = ((i + .5) / (double) size) * 100;
+            interpolatedData[i] = StatUtils.percentile(source, value);
+        }
+        return interpolatedData;
+    }
+}
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/components/Axis.java b/jsplot/src/main/java/tech/tablesaw/plotly/components/Axis.java
index 486590e5..e8b4d5d8 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/components/Axis.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/components/Axis.java
@@ -250,6 +250,8 @@ public class Axis extends Component {
     private final CategoryOrder categoryOrder;
 
     private final TickSettings tickSettings;
+    
+    private final float[] domain;
 
     public static AxisBuilder builder() {
         return new AxisBuilder();
@@ -287,6 +289,7 @@ public class Axis extends Component {
         constrainToward = builder.constrainToward;
         scaleRatio = builder.scaleRatio;
         categoryOrder = builder.categoryOrder;
+        domain = builder.domain;
     }
 
     public String asJavascript() {
@@ -350,6 +353,7 @@ public class Axis extends Component {
         if(showLine != DEFAULT_SHOW_LINE) context.put("showLine", showLine);
         if(zeroLine != DEFAULT_ZERO_LINE) context.put("zeroLine", zeroLine);
         if(showGrid != DEFAULT_SHOW_GRID) context.put("showGrid", showGrid);
+        if (domain != null) { context.put("domain", String.format("[%.2f, %.2f]", domain[0], domain[1])); }
         return context;
     }
 
@@ -391,6 +395,8 @@ public class Axis extends Component {
         private ScatterTrace.YAxis overlaying;
 
         private CategoryOrder categoryOrder;
+        
+        private float[] domain = null;
 
         private AxisBuilder() {}
 
@@ -414,6 +420,11 @@ public class Axis extends Component {
             return this;
         }
 
+        public AxisBuilder domain(float start, float end) {
+            this.domain = new float[]{start, end};
+            return this;
+        }
+
         public AxisBuilder visible(boolean visible) {
             this.visible = visible;
             return this;
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/components/Figure.java b/jsplot/src/main/java/tech/tablesaw/plotly/components/Figure.java
index eaf4348c..88416d31 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/components/Figure.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/components/Figure.java
@@ -4,6 +4,7 @@ import com.mitchellbosecke.pebble.PebbleEngine;
 import com.mitchellbosecke.pebble.error.PebbleException;
 import com.mitchellbosecke.pebble.template.PebbleTemplate;
 import tech.tablesaw.plotly.traces.Trace;
+import tech.tablesaw.plotly.event.EventHandler;
 
 import java.io.IOException;
 import java.io.StringWriter;
@@ -23,6 +24,7 @@ public class Figure {
 
     private final Trace[] data;
     private final Layout layout;
+    private final EventHandler[] eventHandlers;
 
     private final Map<String, Object> context = new HashMap<>();
 
@@ -31,11 +33,25 @@ public class Figure {
     public Figure(Layout layout, Trace... traces) {
         this.data = traces;
         this.layout = layout;
+        this.eventHandlers = null;
+    }
+
+    public Figure(Layout layout, EventHandler eventHandler, Trace... traces) {
+        this.data = traces;
+        this.layout = layout;
+        this.eventHandlers = new EventHandler[] {eventHandler};
+    }
+
+    public Figure(Layout layout, EventHandler[] eventHandlers, Trace... traces) {
+        this.data = traces;
+        this.layout = layout;
+        this.eventHandlers = eventHandlers;
     }
 
     public Figure(Trace... traces) {
         this.data = traces;
         this.layout = null;
+        this.eventHandlers = null;
     }
 
     public String divString(String divName) {
@@ -82,6 +98,7 @@ public class Figure {
         String figure = builder.toString();
         context.put("figure", figure);
         context.put("plotFunction", plotFunction(targetName));
+        context.put("eventHandlerFunction", eventHandlerFunction(targetName, divName));
     }
 
     private String plotFunction(String divName) {
@@ -110,6 +127,20 @@ public class Figure {
         }
 
         builder.append(");");
+        
+        return builder.toString();
+    }
+
+    private String eventHandlerFunction(String targetName, String divName) {
+        StringBuilder builder = new StringBuilder();
+        
+        if (eventHandlers != null) {
+            builder.append(System.lineSeparator());
+            for (int i=0; i < eventHandlers.length; i++) {
+                builder.append(eventHandlers[i].asJavascript(targetName, divName));
+            }
+            builder.append(System.lineSeparator());
+        }
 
         return builder.toString();
     }
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..391089d4 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,47 @@ 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 XSide {
+        BOTTOM,
+        BOTTOM_PLOT,
+        TOP,
+        TOP_PLOT;
+
+        @Override
+        public String toString() {
+            return this.name().toLowerCase().replaceAll("_", " ");
+        }
+    }
+
+    public enum YSide {
+        LEFT,
+        LEFT_PLOT,
+        RIGHT,
+        RIGHT_PLOT;
+
+        @Override
+        public String toString() {
+            return this.name().toLowerCase().replaceAll("_", " ");
+        }
+    }
+
+    public enum Pattern{
+        INDEPENDENT,
+        COUPLED;
+        
+        @Override
+        public String toString() {
+            return this.name().toLowerCase();
+        }
     }
 
     /**
@@ -30,6 +70,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,
@@ -51,12 +100,20 @@ public class Grid extends Component {
      */
     private final double yGap; // number between or equal to 0 and 1
 
+
+    private final XSide xSide;
+
+    private final YSide ySide;
+
     public Grid(GridBuilder gridBuilder) {
         this.rows = gridBuilder.rows;
         this.columns = gridBuilder.columns;
         this.rowOrder = gridBuilder.rowOrder;
         this.xGap = gridBuilder.xGap;
         this.yGap = gridBuilder.yGap;
+        this.pattern = gridBuilder.pattern;
+        this.xSide = gridBuilder.xSide;
+        this.ySide = gridBuilder.ySide;
     }
 
 
@@ -81,10 +138,13 @@ public class Grid extends Component {
         context.put("rows", rows);
         context.put("columns", columns);
         context.put("rowOrder", rowOrder);
+        context.put("pattern", pattern);
+        context.put("xSide", xSide);
+        context.put("ySide", ySide);
         return context;
     }
 
-    public GridBuilder gridBuilder() {
+    public static GridBuilder builder() {
         return new GridBuilder();
     }
 
@@ -98,7 +158,12 @@ public class Grid extends Component {
 
         private double yGap = 80;
 
+        private XSide xSide = XSide.BOTTOM;
+
+        private YSide ySide = YSide.LEFT;
+
         private RowOrder rowOrder = RowOrder.TOP_TO_BOTTOM;
+        private Pattern pattern = Pattern.COUPLED;
 
         private GridBuilder() {}
 
@@ -157,10 +222,43 @@ public class Grid extends Component {
             return this;
         }
 
+        /**
+         * Sets where the y axis labels and titles go. "left" means the very left edge of the grid.
+         * "left plot" is the leftmost plot that each y axis is used in. "right" and "right plot" are similar.
+         */
+        public GridBuilder ySide(YSide ySide) {
+            this.ySide = ySide;
+            return this;
+        }
+
+        /**
+         * Sets where the x axis labels and titles go. "bottom" means the very bottom of the grid.
+         * "bottom plot" is the lowest plot that each x axis is used in. "top" and "top plot" are similar.
+         */
+        public GridBuilder xSide(XSide xSide) {
+            this.xSide = xSide;
+            return this;
+        }
+
         public GridBuilder rowOrder(RowOrder rowOrder) {
             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/event/EventHandler.java b/jsplot/src/main/java/tech/tablesaw/plotly/event/EventHandler.java
new file mode 100644
index 00000000..439a950a
--- /dev/null
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/event/EventHandler.java
@@ -0,0 +1,13 @@
+package tech.tablesaw.plotly.event;
+
+public interface EventHandler {
+
+    /**
+     * Returns a string of Javascript code that implements a plotly event handler
+     * 
+     * @param targetName name of the target document
+     * @param divName    target document id
+     * @return A string that can be rendered in javascript
+     */
+    String asJavascript(String targetName, String divName);
+}
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/event/EventHandlerBody.java b/jsplot/src/main/java/tech/tablesaw/plotly/event/EventHandlerBody.java
new file mode 100644
index 00000000..03570d17
--- /dev/null
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/event/EventHandlerBody.java
@@ -0,0 +1,13 @@
+package tech.tablesaw.plotly.event;
+
+public interface EventHandlerBody {
+
+    /**
+     * Returns a string of Javascript code that implements a plotly event handler
+     * @param targetName     name of the target document
+     * @param divName        target document id
+     * @param eventData      name of the event data variable
+     * @return               A string that can be rendered in javascript
+     */
+    String asJavascript(String targetName, String divName, String eventData);
+}
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/event/HoverBroadcastBody.java b/jsplot/src/main/java/tech/tablesaw/plotly/event/HoverBroadcastBody.java
new file mode 100644
index 00000000..5d79d2e2
--- /dev/null
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/event/HoverBroadcastBody.java
@@ -0,0 +1,69 @@
+package tech.tablesaw.plotly.event;
+
+public class HoverBroadcastBody implements EventHandlerBody {
+
+    private final String[] subPlots;
+    private final int nTraces;
+
+    public static HoverBroadcastBuilder builder() {
+        return new HoverBroadcastBuilder();
+    }
+
+    private HoverBroadcastBody(HoverBroadcastBuilder builder) {
+        this.subPlots = builder.subPlots;
+        this.nTraces = builder.nTraces;
+    }
+
+    @Override
+    public String asJavascript(String targetName, String divName, String eventData) {
+        StringBuilder builder = new StringBuilder();
+
+        builder.append(String.format("\tvar pointIndex = %s.points[0].pointNumber;", eventData));
+        builder.append(System.lineSeparator());
+        builder.append(String.format("\tPlotly.Fx.hover('%s',[ ", divName));
+        builder.append(System.lineSeparator());
+
+        for (int i = 0; i < nTraces; i++) {
+            builder.append(String.format("\t\t{ curveNumber: %d, pointNumber: pointIndex }", i));
+            if (i < nTraces - 1) {
+                builder.append(", ");
+            }
+            builder.append(System.lineSeparator());
+        }
+        builder.append("\t\t]");
+
+        if (subPlots.length > 0) {
+            builder.append(", [");
+            for (int i = 0; i < subPlots.length; i++) {
+                builder.append(String.format("'%s'", subPlots[i]));
+                if (i < subPlots.length - 1) {
+                    builder.append(", ");
+                }
+            }
+            builder.append("]");
+            builder.append(System.lineSeparator());
+        }
+        builder.append("\t);");
+        builder.append(System.lineSeparator());
+        return builder.toString();
+    }
+
+    public static class HoverBroadcastBuilder {
+        private String[] subPlots;
+        private int nTraces;
+
+        public HoverBroadcastBuilder subPlots(String[] subPlots) {
+            this.subPlots = subPlots;
+            return this;
+        }
+
+        public HoverBroadcastBuilder numTraces(int nTraces) {
+            this.nTraces = nTraces;
+            return this;
+        }
+
+        public HoverBroadcastBody build() {
+            return new HoverBroadcastBody(this);
+        }
+    }
+}
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/event/HoverEventHandler.java b/jsplot/src/main/java/tech/tablesaw/plotly/event/HoverEventHandler.java
new file mode 100644
index 00000000..97c930e6
--- /dev/null
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/event/HoverEventHandler.java
@@ -0,0 +1,51 @@
+package tech.tablesaw.plotly.event;
+
+public class HoverEventHandler implements EventHandler {
+
+    private final EventHandlerBody body;
+    private final String eventDataVarName = "eventData";
+
+    public static HoverEventHanlderBuilder builder() {
+        return new HoverEventHanlderBuilder();
+    }
+
+    private HoverEventHandler(HoverEventHanlderBuilder builder) {
+        this.body = builder.body;
+    }
+
+    /**
+     * Returns a string of Javascript code that implements a plotly hover event
+     * handler
+     * 
+     * @param targetName name of the target document
+     * @param divName    target document id
+     * @return A string that can be rendered in javascript
+     */
+    @Override
+    public String asJavascript(String targetName, String divName) {
+        StringBuilder builder = new StringBuilder();
+
+        builder.append(String.format("%s.on('plotly_hover', function(%s){", targetName, eventDataVarName));
+        builder.append(System.lineSeparator());
+
+        builder.append(body.asJavascript(targetName, divName, eventDataVarName));
+
+        builder.append("});");
+        builder.append(System.lineSeparator());
+
+        return builder.toString();
+    }
+
+    public static class HoverEventHanlderBuilder {
+        private EventHandlerBody body;
+
+        public HoverEventHanlderBuilder body(EventHandlerBody body) {
+            this.body = body;
+            return this;
+        }
+
+        public HoverEventHandler build() {
+            return new HoverEventHandler(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..f8d0c306 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/traces/ScatterTrace.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/traces/ScatterTrace.java
@@ -3,10 +3,7 @@ package tech.tablesaw.plotly.traces;
 import com.google.common.base.Preconditions;
 import com.mitchellbosecke.pebble.error.PebbleException;
 import com.mitchellbosecke.pebble.template.PebbleTemplate;
-import tech.tablesaw.api.DateColumn;
-import tech.tablesaw.api.DateTimeColumn;
 import tech.tablesaw.api.NumericColumn;
-import tech.tablesaw.api.TimeColumn;
 import tech.tablesaw.columns.Column;
 import tech.tablesaw.plotly.components.HoverLabel;
 import tech.tablesaw.plotly.components.Line;
@@ -67,7 +64,7 @@ public class ScatterTrace extends AbstractTrace {
 
     private final Fill fill;
     private final String fillColor;
-    private final double[] y;
+    private final Object[] y;
     private final Object[] x;
     private final String[] text;
     private final Mode mode;
@@ -89,10 +86,7 @@ public class ScatterTrace extends AbstractTrace {
         return new ScatterBuilder(x, y);
     }
 
-    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) {
+    public static ScatterBuilder builder(Column<?> x, Column<?> y) {
         return new ScatterBuilder(x, y);
     }
 
@@ -100,14 +94,6 @@ public class ScatterTrace extends AbstractTrace {
         return new ScatterBuilder(x, open, high, low, close);
     }
 
-    public static ScatterBuilder builder(DateTimeColumn x, NumericColumn<? extends Number> y) {
-        return new ScatterBuilder(x, y);
-    }
-
-    public static ScatterBuilder builder(TimeColumn x, NumericColumn<? extends Number> y) {
-        return new ScatterBuilder(x, y);
-    }
-
     private ScatterTrace(ScatterBuilder builder) {
         super(builder);
         this.mode = builder.mode;
@@ -135,24 +121,47 @@ public class ScatterTrace extends AbstractTrace {
         Map<String, Object> context = super.getContext();
         context.put("variableName", "trace" + i);
         context.put("mode", mode);
-        context.put("x", dataAsString(x));
-        if (y != null) context.put("y", dataAsString(y));
+        if (x != null) {
+            context.put("x", dataAsString(x));
+        }
+        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("decreasing", 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());
         }
@@ -207,7 +216,7 @@ public class ScatterTrace extends AbstractTrace {
         private String type = "scatter";
         private Mode mode = Mode.MARKERS;
         private final Object[] x;
-        private double[] y;
+        private Object[] y;
         private String[] text;
         private Marker marker;
         private YAxis yAxis;
@@ -220,47 +229,55 @@ 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;
 
         private ScatterBuilder(double[] x, double[] y) {
             Double[] x1 = new Double[x.length];
+            Double[] y1 = new Double[y.length];
             for (int i = 0; i < x1.length; i++) {
                 x1[i] = x[i];
+                y1[i] = y[i];
             }
             this.x = x1;
-            this.y = y;
+            this.y = y1;
         }
 
-        private ScatterBuilder(Column<?> x, NumericColumn<? extends Number> y) {
+        private ScatterBuilder(Column<?> x, Column<?> y) {
             this.x = x.asObjectArray();
-            this.y = y.asDoubleArray();
+            this.y = y.asObjectArray();
         }
 
         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();
@@ -268,36 +285,23 @@ public class ScatterTrace extends AbstractTrace {
             this.close = close.asDoubleArray();
         }
 
-        private ScatterBuilder(DateColumn x, NumericColumn<? extends Number> y) {
-            this.x = x.asObjectArray();
-            this.y = y.asDoubleArray();
-        }
-
-        private ScatterBuilder(DateTimeColumn x, NumericColumn<? extends Number> y) {
-            this.x = x.asObjectArray();
-            this.y = y.asDoubleArray();
-        }
-
-        private ScatterBuilder(TimeColumn x, NumericColumn<? extends Number> y) {
-            this.x = x.asObjectArray();
-            this.y = y.asDoubleArray();
-        }
-
         public ScatterBuilder mode(Mode mode) {
             this.mode = mode;
             return this;
         }
 
         /**
-         * 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 +348,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 +395,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/axis_template.html b/jsplot/src/main/resources/axis_template.html
index 43fb0827..4f8710d6 100644
--- a/jsplot/src/main/resources/axis_template.html
+++ b/jsplot/src/main/resources/axis_template.html
@@ -95,5 +95,8 @@
 {% endif %}
 {% if categoryOrder is not null %}
     categoryorder: '{{categoryOrder}}'
+{% endif %}
+{% if domain is not null %}
+    domain: {{domain}},
 {% endif %}
     }
\ No newline at end of file
diff --git a/jsplot/src/main/resources/figure_template.html b/jsplot/src/main/resources/figure_template.html
index 07bce19e..965b4061 100644
--- a/jsplot/src/main/resources/figure_template.html
+++ b/jsplot/src/main/resources/figure_template.html
@@ -2,4 +2,5 @@
         var {{targetName}} = document.getElementById('{{divName}}');
         {{figure | raw}}
         {{plotFunction}}
+        {{eventHandlerFunction | raw}}
     </script>
diff --git a/jsplot/src/main/resources/grid_template.html b/jsplot/src/main/resources/grid_template.html
index e79f0093..e1616366 100644
--- a/jsplot/src/main/resources/grid_template.html
+++ b/jsplot/src/main/resources/grid_template.html
@@ -14,4 +14,13 @@
 {% if rowOrder is not null %}
     roworder: '{{rowOrder}}',
 {% endif %}
+{% if xSide is not null %}
+    xside: '{{xSide}}',
+{% endif %}
+{% if ySide is not null %}
+    yside: '{{ySide}}',
+{% 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 %}
diff --git a/pom.xml b/pom.xml
index bfe372ad..e51222d7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
 
     <groupId>tech.tablesaw</groupId>
     <artifactId>tablesaw-parent</artifactId>
-    <version>0.34.1-SNAPSHOT</version>
+    <version>0.34.2-SNAPSHOT</version>
     <name>Tablesaw</name>
     <description>High-performance Java Dataframe</description>
     <url>https://github.com/jtablesaw/tablesaw</url>


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/FloatColumnTest.java b/core/src/test/java/tech/tablesaw/api/FloatColumnTest.java
index b7a847e7..4dcaf0f2 100644
--- a/core/src/test/java/tech/tablesaw/api/FloatColumnTest.java
+++ b/core/src/test/java/tech/tablesaw/api/FloatColumnTest.java
@@ -14,6 +14,7 @@
 
 package tech.tablesaw.api;
 
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
 import org.junit.jupiter.api.Test;
@@ -35,4 +36,16 @@ public class FloatColumnTest {
         floatColumn.append(floatObject);
         assertEquals(floatObject, floatColumn.get(0));
     }
+
+    @Test
+    public void testMapToDouble() {
+        FloatColumn column = FloatColumn.create("testing");
+        column.append(1.0f);
+        column.append(2.0f);
+        column.append(3.0f);
+
+        DoubleColumn actual = column.mapToDoubleColumn("doubles", (x) -> 2.0 * x);
+
+        assertArrayEquals(new double[]{2.0, 4.0, 6.0}, actual.asDoubleArray());
+    }
 }
diff --git a/core/src/test/java/tech/tablesaw/api/NumberColumnTest.java b/core/src/test/java/tech/tablesaw/api/NumberColumnTest.java
index b0bce44a..2d641924 100644
--- a/core/src/test/java/tech/tablesaw/api/NumberColumnTest.java
+++ b/core/src/test/java/tech/tablesaw/api/NumberColumnTest.java
@@ -664,22 +664,29 @@ public class NumberColumnTest {
         assertFalse(DoubleColumn.create("t1", new double[] {1, 0, -1}).noneMatch(isNegativeD));
     }
 
-    private <T> void check(Column<T> column, @SuppressWarnings("unchecked") T... ts) {
-        assertEquals(ts.length, column.size());
-        for (int i = 0; i < ts.length; i++) {
-            assertEquals(ts[i], column.get(i));
+    private <T> void assertColumnContentsEquals(T[] expected, Column<T> column) {
+        assertEquals(expected.length, column.size());
+        for (int i = 0; i < expected.length; i++) {
+            assertEquals(expected[i], column.get(i));
         }
     }
 
     @Test
     public void testFilter() {
         Column<Double> filtered = DoubleColumn.create("t1", new double[] {-1, 0, 1}).filter(isPositiveOrZeroD);
-        check(filtered, 0.0, 1.0);
+        assertColumnContentsEquals(new Double[] { 0.0, 1.0 }, filtered);
+    }
+
+    @Test
+    public void testMap() {
+        DoubleColumn mapped = DoubleColumn.create("t1", new double[] {-1, 0, 1}).map(x -> x * 2.0 + 1.0);
+        assertColumnContentsEquals(new Double[] { -1.0, 1.0, 3.0 }, mapped);
     }
 
     @Test
     public void testMapInto() {
-        check(DoubleColumn.create("t1", new double[] {-1, 0, 1}).mapInto(toStringD, StringColumn.create("result")), "-1.0", "0.0", "1.0");
+        StringColumn mapped = DoubleColumn.create("t1", new double[] {-1, 0, 1}).mapInto(toStringD, StringColumn.create("result"));
+        assertColumnContentsEquals(new String[] { "-1.0", "0.0", "1.0" }, mapped);
     }
 
     @Test
diff --git a/core/src/test/java/tech/tablesaw/api/StringColumnTest.java b/core/src/test/java/tech/tablesaw/api/StringColumnTest.java
index 17ebbf26..09d955b9 100644
--- a/core/src/test/java/tech/tablesaw/api/StringColumnTest.java
+++ b/core/src/test/java/tech/tablesaw/api/StringColumnTest.java
@@ -14,29 +14,25 @@
 
 package tech.tablesaw.api;
 
-import static org.junit.jupiter.api.Assertions.assertArrayEquals;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static tech.tablesaw.columns.strings.StringPredicates.isEqualToIgnoringCase;
-
-import java.util.Arrays;
-import java.util.List;
-import java.util.function.Function;
-
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
-
 import tech.tablesaw.TestDataUtil;
 import tech.tablesaw.columns.strings.StringColumnFormatter;
 import tech.tablesaw.selection.Selection;
 
-public class StringColumnTest {
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Function;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static tech.tablesaw.columns.strings.StringPredicates.isEqualToIgnoringCase;
+
+class StringColumnTest {
 
     private final StringColumn column = StringColumn.create("testing");
 
     @BeforeEach
-    public void setUp() {
+    void setUp() {
         column.append("Value 1");
         column.append("Value 2");
         column.append("Value 3");
@@ -46,7 +42,7 @@ public class StringColumnTest {
 /*
 TODO: fix
     @Test
-    public void testSummarizeIf() {
+    void testSummarizeIf() {
         double result = column.summarizeIf(
                 column.endsWith("3").or(column.endsWith("4")),
                 count);
@@ -58,13 +54,13 @@ TODO: fix
 */
 
     @Test
-    public void testAppendObj2() {
+    void testAppendObj2() {
         final StringColumn sc = StringColumn.create("sc", Arrays.asList("a", "b", "c", "a"));
         assertArrayEquals(sc.asList().toArray(), sc.asObjectArray());
     }
 
     @Test
-    public void testForNulls() {
+    void testForNulls() {
         String[] array1 = {"1", "2", "3", "4", null};
         Table table1 = Table.create("table1", StringColumn.create("id", array1));
         assertEquals("", table1.stringColumn("id").get(4));
@@ -75,7 +71,7 @@ TODO: fix
     }
 
     @Test
-    public void testAppendObj() {
+    void testAppendObj() {
         StringColumn column = StringColumn.create("testing");
         column.appendObj("Value 1");
         column.appendObj(null);
@@ -84,14 +80,14 @@ TODO: fix
     }
     
     @Test
-    public void testConditionalSet() {
+    void testConditionalSet() {
         column.set(column.isEqualTo("Value 4"), "no Value");
         assertTrue(column.contains("no Value"));
         assertFalse(column.contains("Value 4"));
     }
 
     @Test
-    public void lag() {
+    void lag() {
         StringColumn c1 = column.lag(1);
         Table t = Table.create("Test");
         t.addColumns(column, c1);
@@ -101,7 +97,7 @@ TODO: fix
     }
 
     @Test
-    public void lag2() {
+    void lag2() {
         StringColumn c1 = column.lag(-1);
         Table t = Table.create("Test");
         t.addColumns(column, c1);
@@ -111,7 +107,7 @@ TODO: fix
     }
 
     @Test
-    public void lead() {
+    void lead() {
         StringColumn c1 = column.lead(1);
         Table t = Table.create("Test");
         t.addColumns(column, c1);
@@ -121,58 +117,58 @@ TODO: fix
     }
 
     @Test
-    public void testSelectWhere() {
+    void testSelectWhere() {
         StringColumn result = column.where(column.equalsIgnoreCase("VALUE 1"));
         assertEquals(1, result.size());
     }
 
     @Test
-    public void testDefaultReturnValue() {
+    void testDefaultReturnValue() {
         assertEquals(-1, column.firstIndexOf("test"));
     }
 
     @Test
-    public void testType() {
+    void testType() {
         assertEquals(ColumnType.STRING, column.type());
     }
 
     @Test
-    public void testGetString() {
+    void testGetString() {
         assertEquals("Value 2", column.getString(1));
     }
 
     @Test
-    public void testSize() {
+    void testSize() {
         assertEquals(4, column.size());
     }
 
     @Test
-    public void testGetDummies() {
+    void testGetDummies() {
         List<BooleanColumn> dummies = column.getDummies();
         assertEquals(4, dummies.size());
     }
 
     @Test
-    public void testToString() {
+    void testToString() {
         assertEquals("String column: testing", column.toString());
     }
 
     @Test
-    public void testMax() {
+    void testMax() {
         StringColumn stringColumn = StringColumn.create("US States");
         stringColumn.addAll(TestDataUtil.usStates());
         assertEquals("Wyoming", stringColumn.top(5).get(0));
     }
 
     @Test
-    public void testMin() {
+    void testMin() {
         StringColumn stringColumn = StringColumn.create("US States");
         stringColumn.addAll(TestDataUtil.usStates());
         assertEquals("Alabama", stringColumn.bottom(5).get(0));
     }
 
     @Test
-    public void testStartsWith() {
+    void testStartsWith() {
         StringColumn stringColumn = StringColumn.create("US States");
         stringColumn.addAll(TestDataUtil.usStates());
 
@@ -188,7 +184,7 @@ TODO: fix
     }
 
     @Test
-    public void testFormattedPrinting() {
+    void testFormattedPrinting() {
         StringColumn stringColumn = StringColumn.create("US States");
         stringColumn.addAll(TestDataUtil.usStates());
 
@@ -199,7 +195,7 @@ TODO: fix
     }
 
     @Test
-    public void testSelectWithFilter() {
+    void testSelectWithFilter() {
         StringColumn stringColumn = StringColumn.create("US States");
         stringColumn.addAll(TestDataUtil.usStates());
 
@@ -212,7 +208,7 @@ TODO: fix
     }
 
     @Test
-    public void testIsNotEqualTo() {
+    void testIsNotEqualTo() {
         StringColumn stringColumn = StringColumn.create("US States");
         stringColumn.addAll(TestDataUtil.usStates());
 
@@ -224,7 +220,7 @@ TODO: fix
     }
 
     @Test
-    public void testColumnEqualIgnoringCase() {
+    void testColumnEqualIgnoringCase() {
         StringColumn other = column.copy();
         other.set(1, "Some other thing");
         other.set(2, other.get(2).toUpperCase());
@@ -240,7 +236,7 @@ TODO: fix
     }
 
     @Test
-    public void testIsEqualTo() {
+    void testIsEqualTo() {
         StringColumn stringColumn = StringColumn.create("US States");
         stringColumn.addAll(TestDataUtil.usStates());
         stringColumn.append("Alabama");  // so we have two entries
@@ -257,7 +253,7 @@ TODO: fix
     }
 
     @Test
-    public void testIsNotEqualTo2() {
+    void testIsNotEqualTo2() {
         StringColumn stringColumn = StringColumn.create("US States");
         stringColumn.addAll(TestDataUtil.usStates());
 
@@ -268,7 +264,7 @@ TODO: fix
     }
 
     @Test
-    public void testIsIn() {
+    void testIsIn() {
         StringColumn stringColumn = StringColumn.create("US States");
         stringColumn.addAll(TestDataUtil.usStates());
         StringColumn selection = stringColumn.where(stringColumn.isIn("Alabama", "Texas"));
@@ -278,7 +274,7 @@ TODO: fix
     }
 
     @Test
-    public void testIsNotIn() {
+    void testIsNotIn() {
         StringColumn stringColumn = StringColumn.create("US States");
         stringColumn.addAll(TestDataUtil.usStates());
         StringColumn selection = stringColumn.where(stringColumn.isNotIn("Alabama", "Texas"));
@@ -289,7 +285,7 @@ TODO: fix
     }
 
     @Test
-    public void testToList() {
+    void testToList() {
         StringColumn stringColumn = StringColumn.create("US States");
         stringColumn.addAll(TestDataUtil.usStates());
         List<String> states = stringColumn.asList();
@@ -297,7 +293,7 @@ TODO: fix
     }
 
     @Test
-    public void testFormatting() {
+    void testFormatting() {
         String[] names = {"John White", "George Victor"};
         StringColumn nameColumn = StringColumn.create("names", names);
         StringColumn formatted = nameColumn.format("Name: %s");
@@ -305,7 +301,7 @@ TODO: fix
     }
 
     @Test
-    public void testDistance() {
+    void testDistance() {
         String[] words = {"canary", "banana", "island", "reggae"};
         String[] words2 = {"cancel", "bananas", "islander", "calypso"};
         StringColumn wordColumn = StringColumn.create("words", words);
@@ -316,7 +312,7 @@ TODO: fix
     }
 
     @Test
-    public void testCommonSuffix() {
+    void testCommonSuffix() {
         String[] words = {"running", "icecube", "regular", "reggae"};
         String[] words2 = {"rowing", "cube", "premium", "place"};
         StringColumn wordColumn = StringColumn.create("words", words);
@@ -328,7 +324,7 @@ TODO: fix
     }
 
     @Test
-    public void testCommonPrefix() {
+    void testCommonPrefix() {
         String[] words = {"running", "icecube", "back"};
         String[] words2 = {"rowing", "iceland", "backup"};
         StringColumn wordColumn = StringColumn.create("words", words);
@@ -340,7 +336,7 @@ TODO: fix
     }
 
     @Test
-    public void testPadStart() {
+    void testPadStart() {
         String[] words = {"running", "icecube", "back"};
         StringColumn wordColumn = StringColumn.create("words", words);
         StringColumn result = wordColumn.padStart(8, ' ');
@@ -350,7 +346,7 @@ TODO: fix
     }
 
     @Test
-    public void testPadEnd() {
+    void testPadEnd() {
         String[] words = {"running", "icecube", "back"};
         StringColumn wordColumn = StringColumn.create("words", words);
         StringColumn result = wordColumn.padEnd(8, 'X');
@@ -360,7 +356,37 @@ TODO: fix
     }
 
     @Test
-    public void testSubstring() {
+    void testParseInt() {
+        String[] values = {"4", "72", "132"};
+        StringColumn wordColumn = StringColumn.create("values", values);
+        IntColumn result = wordColumn.parseInt();
+        assertEquals(result.get(0), 4);
+        assertEquals(result.get(1), 72);
+        assertEquals(result.get(2), 132);
+    }
+
+    @Test
+    void testParseDouble() {
+        String[] values = {"0.4", "0.72", "1.132"};
+        StringColumn wordColumn = StringColumn.create("values", values);
+        DoubleColumn result = wordColumn.parseDouble();
+        assertEquals(result.get(0), 0.4);
+        assertEquals(result.get(1), 0.72);
+        assertEquals(result.get(2), 1.132);
+    }
+
+    @Test
+    void testParseFloat() {
+        String[] values = {"0.4", "0.72", "1.132"};
+        StringColumn wordColumn = StringColumn.create("values", values);
+        FloatColumn result = wordColumn.parseFloat();
+        assertEquals(result.get(0), 0.4f);
+        assertEquals(result.get(1), 0.72f);
+        assertEquals(result.get(2), 1.132f);
+    }
+
+    @Test
+    void testSubstring() {
         String[] words = {"running", "icecube", "back"};
         StringColumn wordColumn = StringColumn.create("words", words);
         StringColumn result = wordColumn.substring(3);
@@ -370,7 +396,7 @@ TODO: fix
     }
 
     @Test
-    public void testSubstring2() {
+    void testSubstring2() {
         String[] words = {"running", "icecube", "back"};
         StringColumn wordColumn = StringColumn.create("words", words);
         StringColumn result = wordColumn.substring(1,3);
@@ -380,7 +406,7 @@ TODO: fix
     }
 
     @Test
-    public void testReplaceFirst() {
+    void testReplaceFirst() {
         String[] words = {"running", "run run run"};
         StringColumn wordColumn = StringColumn.create("words", words);
         StringColumn result = wordColumn.replaceFirst("run","walk");
@@ -389,7 +415,7 @@ TODO: fix
     }
 
     @Test
-    public void testReplaceAll() {
+    void testReplaceAll() {
         String[] words = {"running", "run run run"};
         StringColumn wordColumn = StringColumn.create("words", words);
         StringColumn result = wordColumn.replaceAll("run","walk");
@@ -398,7 +424,7 @@ TODO: fix
     }
 
     @Test
-    public void testReplaceAll2() {
+    void testReplaceAll2() {
         String[] words = {"running", "run run run"};
         String[] regex = {"n", "g"};
         StringColumn wordColumn = StringColumn.create("words", words);
@@ -408,7 +434,7 @@ TODO: fix
     }
 
     @Test
-    public void testJoin() {
+    void testJoin() {
         String[] words = {"running", "run"};
         String[] words2 = {"walking", "walk"};
         String[] words3 = {"swimming", "swim"};
@@ -421,7 +447,7 @@ TODO: fix
     }
 
     @Test
-    public void testTrim() {
+    void testTrim() {
         String[] words = {" running ", " run run run "};
         StringColumn wordColumn = StringColumn.create("words", words);
         StringColumn result = wordColumn.trim();
@@ -430,7 +456,7 @@ TODO: fix
     }
 
     @Test
-    public void testUpperCase() {
+    void testUpperCase() {
         String[] words = {"running", "run run run"};
         StringColumn wordColumn = StringColumn.create("words", words);
         StringColumn result = wordColumn.upperCase();
@@ -439,7 +465,7 @@ TODO: fix
     }
 
     @Test
-    public void testLowerCase() {
+    void testLowerCase() {
         String[] words = {"RUNNING", "RUN RUN RUN"};
         StringColumn wordColumn = StringColumn.create("words", words);
         StringColumn result = wordColumn.lowerCase();
@@ -448,7 +474,7 @@ TODO: fix
     }
 
     @Test
-    public void testAbbreviate() {
+    void testAbbreviate() {
         String[] words = {"running", "Stop Breaking Down", "Backwards Writing"};
         StringColumn wordColumn = StringColumn.create("words", words);
         StringColumn result = wordColumn.abbreviate(10);
@@ -458,7 +484,7 @@ TODO: fix
     }
 
     @Test
-    public void tokenizeAndSort() {
+    void tokenizeAndSort() {
         String[] words = {"Stop Breaking Down", "Backwards Writing"};
         StringColumn wordColumn = StringColumn.create("words", words);
         StringColumn result = wordColumn.tokenizeAndSort();
@@ -467,7 +493,7 @@ TODO: fix
     }
 
     @Test
-    public void tokenizeAndSort1() {
+    void tokenizeAndSort1() {
         String[] words = {"Stop,Breaking,Down", "Writing Backwards"};
         StringColumn wordColumn = StringColumn.create("words", words);
         StringColumn result = wordColumn.tokenizeAndSort(",");
@@ -476,7 +502,7 @@ TODO: fix
     }
 
     @Test
-    public void tokenizeAndRemoveDuplicates() {
+    void tokenizeAndRemoveDuplicates() {
         String[] words = {"Stop Breaking Stop Down", "walk run run"};
         StringColumn wordColumn = StringColumn.create("words", words);
         StringColumn result = wordColumn.tokenizeAndRemoveDuplicates(" ");
@@ -485,7 +511,7 @@ TODO: fix
     }
 
     @Test
-    public void chainMaps() {
+    void chainMaps() {
         String[] words = {"Stop Breaking Stop Down", "walk run run"};
         StringColumn wordColumn = StringColumn.create("words", words);
         StringColumn result = wordColumn.tokenizeAndRemoveDuplicates(" ").tokenizeAndSort();
@@ -494,7 +520,7 @@ TODO: fix
     }
 
     @Test
-    public void chainMaps1() {
+    void chainMaps1() {
         String[] words = {"foo", "bar"};
         StringColumn wordColumn = StringColumn.create("words", words);
         StringColumn result = wordColumn.concatenate(" bam");
@@ -503,7 +529,7 @@ TODO: fix
     }
 
     @Test
-    public void asDoubleColumn() {
+    void asDoubleColumn() {
         String[] words = {"foo", "bar", "larry", "foo", "lion", "ben", "tiger", "bar"};
         StringColumn wordColumn = StringColumn.create("words", words);
         DoubleColumn result = wordColumn.asDoubleColumn();
@@ -511,7 +537,7 @@ TODO: fix
     }
 
     @Test
-    public void asDoubleArray() {
+    void asDoubleArray() {
         String[] words = {"foo", "bar", "larry", "foo", "lion", null, "ben", "tiger", "bar"};
         StringColumn wordColumn = StringColumn.create("words", words);
         double[] result = wordColumn.asDoubleArray();
@@ -519,7 +545,7 @@ TODO: fix
     }
 
     @Test
-    public void getDouble() {
+    void getDouble() {
         String[] words = {"foo", "bar", "larry", "foo", "lion", null, "ben", "tiger", "bar"};
         StringColumn wordColumn = StringColumn.create("words", words);
         double[] expected = new double[] { 0.0, 1.0, 2.0, 0.0, 3.0, 4.0, 5.0, 6.0, 1.0 };
@@ -529,4 +555,110 @@ TODO: fix
         }
         assertArrayEquals(expected, result, 0.000_000_1);
     }
+
+    @Test
+    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
+    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
+    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
+    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" + 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
+    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
+    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" + 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 09e7ce84..b3233b48 100644
--- a/core/src/test/java/tech/tablesaw/api/TableTest.java
+++ b/core/src/test/java/tech/tablesaw/api/TableTest.java
@@ -14,12 +14,16 @@
 
 package tech.tablesaw.api;
 
-import com.google.common.collect.Lists;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import tech.tablesaw.columns.Column;
-import tech.tablesaw.columns.dates.PackedLocalDate;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+import static tech.tablesaw.aggregate.AggregateFunctions.mean;
+import static tech.tablesaw.aggregate.AggregateFunctions.stdDev;
 
 import java.io.IOException;
 import java.time.LocalDate;
@@ -31,15 +35,14 @@ import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
 
-import static org.junit.jupiter.api.Assertions.assertArrayEquals;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertSame;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static tech.tablesaw.aggregate.AggregateFunctions.mean;
-import static tech.tablesaw.aggregate.AggregateFunctions.stdDev;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import com.google.common.collect.Lists;
+
+import tech.tablesaw.columns.Column;
+import tech.tablesaw.columns.dates.PackedLocalDate;
+import tech.tablesaw.io.csv.CsvReadOptions;
 
 public class TableTest {
 
@@ -52,13 +55,13 @@ public class TableTest {
     private DoubleColumn numberColumn =  DoubleColumn.create("d1");
 
     @BeforeEach
-    public void setUp() {
+    void setUp() {
         table = Table.create("t");
         table.addColumns(f1);
     }
 
     @Test
-    public void testSummarize() throws Exception {
+    void testSummarize() throws Exception {
         Table table = Table.read().csv("../data/tornadoes_1950-2014.csv");
         Table result = table.summarize("Injuries", mean, stdDev).by("State");
         assertEquals(49, result.rowCount());
@@ -67,12 +70,13 @@ public class TableTest {
     }
 
     @Test
-    public void testColumn() {
+    void testColumn() {
         Column<?> column1 = table.column(0);
         assertNotNull(column1);
     }
 
-    public void testColumnSizeCheck() {
+    @Test
+    void testColumnSizeCheck() {
         assertThrows(IllegalArgumentException.class, () -> {
             double[] a = {3, 4};
             double[] b = {3, 4, 5};
@@ -83,7 +87,7 @@ public class TableTest {
     }
 
     @Test
-    public void testRowWiseAddition() {
+    void testRowWiseAddition() {
         double[] a = {3, 4, 5};
         double[] b = {3, 4, 5};
         double[] c = {3, 4, 5};
@@ -103,7 +107,7 @@ public class TableTest {
     }
 
     @Test
-    public void testRowWiseAddition2() {
+    void testRowWiseAddition2() {
         double[] a = {3, 4, 5};
         double[] b = {3, 4, 5};
         double[] c = {3, 4, 5};
@@ -123,7 +127,7 @@ public class TableTest {
     }
 
     @Test
-    public void testRemoveColumns() {
+    void testRemoveColumns() {
         StringColumn sc = StringColumn.create("0");
         StringColumn sc1 = StringColumn.create("1");
         StringColumn sc2 = StringColumn.create("2");
@@ -137,7 +141,7 @@ public class TableTest {
     }
 
     @Test
-    public void printEmptyTable() {
+    void printEmptyTable() {
         Table t = Table.create("Test");
         assertEquals("Test" + LINE_END + LINE_END, t.print());
 
@@ -147,7 +151,7 @@ public class TableTest {
     }
 
     @Test
-    public void testDropDuplicateRows() throws Exception {
+    void testDropDuplicateRows() throws Exception {
         Table t1 = Table.read().csv("../data/bush.csv");
         int rowCount = t1.rowCount();
         Table t2 = Table.read().csv("../data/bush.csv");
@@ -159,7 +163,7 @@ public class TableTest {
     }
 
     @Test
-    public void testMissingValueCounts() {
+    void testMissingValueCounts() {
         StringColumn c1 = StringColumn.create("SC");
         DoubleColumn c2 = DoubleColumn.create("NC");
         DateColumn c3 = DateColumn.create("DC");
@@ -168,7 +172,7 @@ public class TableTest {
     }
 
     @Test
-    public void testFullCopy() {
+    void testFullCopy() {
         numberColumn.append(2.23424);
         Table t = Table.create("test");
         t.addColumns(numberColumn);
@@ -179,13 +183,13 @@ public class TableTest {
     }
 
     @Test
-    public void testColumnCount() {
+    void testColumnCount() {
         assertEquals(0, Table.create("t").columnCount());
         assertEquals(1, table.columnCount());
     }
 
     @Test
-    public void testLast() throws IOException {
+    void testLast() throws IOException {
         Table t = Table.read().csv("../data/bush.csv");
         t = t.sortOn("date");
         Table t1 = t.last(3);
@@ -194,14 +198,14 @@ public class TableTest {
     }
 
     @Test
-    public void testSelect1() throws Exception {
+    void testSelect1() throws Exception {
         Table t = Table.read().csv("../data/bush.csv");
         Table t1 = t.select(t.column(1), t.column(2));
         assertEquals(2, t1.columnCount());
     }
 
     @Test
-    public void testSelect2() throws Exception {
+    void testSelect2() throws Exception {
         Table t = Table.read().csv("../data/bush.csv");
         Table t1 = t.select(t.column(0), t.column(1), t.column(2), t.dateColumn(0).year());
         assertEquals(4, t1.columnCount());
@@ -209,14 +213,25 @@ public class TableTest {
     }
 
     @Test
-    public void testSampleSplit() throws Exception {
+    void testSampleSplit() throws Exception {
         Table t = Table.read().csv("../data/bush.csv");
         Table[] results = t.sampleSplit(.75);
         assertEquals(t.rowCount(), results[0].rowCount() + results[1].rowCount());
     }
+    
+   @Test
+    void testStratifiedSampleSplit() throws Exception {
+        Table t = Table.read().csv("../data/bush.csv");
+        Table[] results = t.stratifiedSampleSplit(t.stringColumn("who"), .75);
+        assertEquals(t.rowCount(), results[0].rowCount() + results[1].rowCount());
+        int totalFoxCount = t.where(t.stringColumn("who").equalsIgnoreCase("fox")).rowCount();
+        int stratifiedFoxCount = results[0].where(results[0].stringColumn("who").equalsIgnoreCase("fox")).rowCount();
+        
+        assertEquals(.75, (double) stratifiedFoxCount/totalFoxCount, 0.0);
+    }
 
     @Test
-    public void testDoWithEachRow() throws Exception {
+    void testDoWithEachRow() throws Exception {
         Table t = Table.read().csv(CsvReadOptions.builder("../data/bush.csv").minimizeColumnSizes()).first(10);
         Short[] ratingsArray = {53, 58};
         List<Short> ratings = Lists.asList((short) 52, ratingsArray);
@@ -226,11 +241,11 @@ public class TableTest {
                 assertTrue(ratings.contains(row.getShort("approval")));
             }
         };
-        t.doWithRows(doable);
+        t.stream().forEach(doable);
     }
 
     @Test
-    public void testDoWithEachRow2() throws Exception {
+    void testDoWithEachRow2() throws Exception {
         Table t = Table.read().csv(CsvReadOptions.builder("../data/bush.csv").minimizeColumnSizes());
         int dateTarget = PackedLocalDate.pack(LocalDate.of(2002, 1, 1));
         double ratingTarget = 75;
@@ -241,23 +256,23 @@ public class TableTest {
                 count.getAndIncrement();
             }
         };
-        t.doWithRows(doable);
+        t.stream().forEach(doable);
         assertTrue(count.get() > 0);
     }
 
     @Test
-    public void testDetect() throws Exception {
+    void testDetect() throws Exception {
         Table t = Table.read().csv(CsvReadOptions.builder("../data/bush.csv").minimizeColumnSizes());
         int dateTarget = PackedLocalDate.pack(LocalDate.of(2002, 1, 1));
         double ratingTarget = 75;
         Predicate<Row> doable = row ->
                 (row.getPackedDate("date") > dateTarget
                 && row.getShort("approval") > ratingTarget);
-        assertTrue(t.detect(doable));
+        assertTrue(t.stream().anyMatch(doable));
     }
 
     @Test
-    public void testRowToString() throws Exception {
+    void testRowToString() throws Exception {
         Table t = Table.read().csv("../data/bush.csv");
         Row row = new Row(t);
         row.at(0);
@@ -268,14 +283,14 @@ public class TableTest {
     }
 
     @Test
-    public void testPairs() throws Exception {
+    void testPairs() throws Exception {
         Table t = Table.read().csv(CsvReadOptions.builder("../data/bush.csv").minimizeColumnSizes());
         PairChild pairs = new PairChild();
         t.doWithRows(pairs);
     }
 
     @Test
-    public void testPairs2() throws Exception {
+    void testPairs2() throws Exception {
         Table t = Table.read().csv(CsvReadOptions.builder("../data/bush.csv").minimizeColumnSizes());
 
         Table.Pairs runningAvg =  new Table.Pairs() {
@@ -299,7 +314,7 @@ public class TableTest {
     }
 
     @Test
-    public void stepWithRows() throws Exception {
+    void stepWithRows() throws Exception {
         Table t = Table.read()
                 .csv(CsvReadOptions.builder("../data/bush.csv").minimizeColumnSizes())
                 .first(6);
@@ -329,7 +344,7 @@ public class TableTest {
     }
 
     @Test
-    public void testRollWithNrows2() throws Exception {
+    void testRollWithNrows2() throws Exception {
         Table t = Table.read().csv(CsvReadOptions.builder("../data/bush.csv").minimizeColumnSizes()).first(4);
         ShortColumn approval = t.shortColumn("approval");
 
@@ -360,7 +375,7 @@ public class TableTest {
     }
 
     @Test
-    public void testRowCount() {
+    void testRowCount() {
         assertEquals(0, table.rowCount());
         DoubleColumn floatColumn = this.f1;
         floatColumn.append(2f);
@@ -370,19 +385,19 @@ public class TableTest {
     }
 
     @Test
-    public void testAppend() {
+    void testAppend() {
         int appendedRows = appendRandomlyGeneratedColumn(table);
         assertTableColumnSize(table, f1, appendedRows);
     }
 
     @Test
-    public void testAppendEmptyTable() {
+    void testAppendEmptyTable() {
         appendEmptyColumn(table);
         assertTrue(table.isEmpty());
     }
 
     @Test
-    public void testAppendToNonEmptyTable() {
+    void testAppendToNonEmptyTable() {
         populateColumn(f1);
         assertFalse(table.isEmpty());
         int initialSize = table.rowCount();
@@ -391,7 +406,7 @@ public class TableTest {
     }
 
     @Test
-    public void testAppendEmptyTableToNonEmptyTable() {
+    void testAppendEmptyTableToNonEmptyTable() {
         populateColumn(f1);
         assertFalse(table.isEmpty());
         int initialSize = table.rowCount();
@@ -400,7 +415,19 @@ public class TableTest {
     }
 
     @Test
-    public void testAppendMultipleColumns() {
+    void testAppendRow() throws Exception {
+        Table table = Table.read().csv("../data/bush.csv");
+        for (int i = 0; i < 2; i++) {
+            Row row = table.appendRow();
+            row.setString("who", "me");
+            row.setDate("date", LocalDate.now());
+            row.setInt("approval", 5);
+        }
+        assertEquals(5, table.intColumn("approval").get(table.rowCount() - 1));
+    }
+
+    @Test
+    void testAppendMultipleColumns() {
         DoubleColumn column =  DoubleColumn.create("e1");
         table.addColumns(column);
         DoubleColumn first = f1.emptyCopy();
@@ -414,20 +441,20 @@ public class TableTest {
         assertTableColumnSize(table, column, secondColumnSize);
     }
 
-    public void testAppendNull() {
+    void testAppendNull() {
         assertThrows(NullPointerException.class, () -> {
             table.append(null);
         });
     }
 
-    public void testAppendTableWithNonExistingColumns() {
+    void testAppendTableWithNonExistingColumns() {
         assertThrows(IllegalStateException.class, () -> {
             Table tableToAppend = Table.create("wrong", numberColumn);
             table.append(tableToAppend);
         });
     }
 
-    public void testAppendTableWithAnotherColumnName() {
+    void testAppendTableWithAnotherColumnName() {
         assertThrows(IllegalStateException.class, () -> {
             DoubleColumn column =  DoubleColumn.create("42");
             Table tableToAppend = Table.create("wrong", column);
@@ -435,7 +462,7 @@ public class TableTest {
         });
     }
 
-    public void testAppendTableWithDifferentShape() {
+    void testAppendTableWithDifferentShape() {
         assertThrows(IllegalStateException.class, () -> {
             DoubleColumn column =  DoubleColumn.create("e1");
             table.addColumns(column);
@@ -447,7 +474,7 @@ public class TableTest {
     }
 
     @Test
-    public void testReplaceColumn() {
+    void testReplaceColumn() {
         DoubleColumn first =  DoubleColumn.create("c1", new double[]{1, 2, 3, 4, 5});
         DoubleColumn second =  DoubleColumn.create("c2", new double[]{6, 7, 8, 9, 10});
         DoubleColumn replacement =  DoubleColumn.create("c2", new double[]{10, 20, 30, 40, 50});
@@ -498,7 +525,7 @@ public class TableTest {
     }
 
     @Test
-    public void testAsMatrix() {
+    void testAsMatrix() {
         DoubleColumn first =  DoubleColumn.create("c1", new double[]{1L, 2L, 3L, 4L, 5L});
         DoubleColumn second =  DoubleColumn.create("c2", new double[]{6.0f, 7.0f, 8.0f, 9.0f, 10.0f});
         DoubleColumn third =  DoubleColumn.create("c3", new double[]{10.0, 20.0, 30.0, 40.0, 50.0});
@@ -514,7 +541,7 @@ public class TableTest {
     }
 
     @Test
-    public void testRowSort() throws Exception {
+    void testRowSort() throws Exception {
         Table bush = Table.read().csv(CsvReadOptions.builder("../data/bush.csv").minimizeColumnSizes());
 
         Comparator<Row> rowComparator = Comparator.comparingDouble(o -> o.getShort("approval"));
@@ -527,7 +554,7 @@ public class TableTest {
     }
 
     @Test
-    public void testIterable() throws Exception {
+    void testIterable() throws Exception {
         Table bush = Table.read().csv("../data/bush.csv");
         int rowNumber = 0;
         for (Row row : bush.first(10)) {
@@ -535,6 +562,68 @@ public class TableTest {
         }
     }
 
+    @Test
+    void testCountBy1() throws Exception {
+        Table bush = Table.read().csv("../data/bush.csv");
+        Table result = bush.countBy(bush.categoricalColumn("who"));
+        assertEquals(bush.categoricalColumn("who").countUnique(), result.rowCount());
+    }
+
+    @Test
+    void testCountBy2() throws Exception {
+        Table bush = Table.read().csv("../data/bush.csv");
+        Table result = bush.countBy("who");
+        assertEquals(bush.categoricalColumn("who").countUnique(), result.rowCount());
+    }
+
+    @Test
+    void dropRangeStarting() throws IOException {
+        Table table = Table.read().csv(CsvReadOptions.builder("../data/bush.csv"));
+        Table result = table.dropRange(20);
+        assertEquals(table.rowCount() - 20, result.rowCount());
+        for (Column<?> c: result.columns()) {
+            for (int r = 0; r < result.rowCount(); r++) {
+                assertEquals(result.getString(r, c.name()), table.getString(r + 20, c.name()));
+            }
+        }
+    }
+
+    @Test
+    void dropRangeEnding() throws IOException {
+        Table table = Table.read().csv(CsvReadOptions.builder("../data/bush.csv"));
+        Table result = table.dropRange(-20);
+        assertEquals(table.rowCount() - 20, result.rowCount());
+        for (Column<?> c: result.columns()) {
+            for (int r = 0; r < result.rowCount(); r++) {
+                assertEquals(result.getString(r, c.name()), table.getString(r, c.name()));
+            }
+        }
+    }    
+
+    @Test
+    void inRangeStarting() throws IOException {
+        Table table = Table.read().csv(CsvReadOptions.builder("../data/bush.csv"));
+        Table result = table.inRange(20);
+        assertEquals(20, result.rowCount());
+        for (Column<?> c: result.columns()) {
+            for (int r = 0; r < result.rowCount(); r++) {
+                assertEquals(result.getString(r, c.name()), table.getString(r, c.name()));
+            }
+        }
+    }
+
+    @Test
+    void inRangeEnding() throws IOException {
+        Table table = Table.read().csv(CsvReadOptions.builder("../data/bush.csv"));
+        Table result = table.inRange(-20);
+        assertEquals(20, result.rowCount());
+        for (Column<?> c: result.columns()) {
+            for (int r = 0; r < result.rowCount(); r++) {
+                assertEquals(result.getString(r, c.name()), table.getString(table.rowCount() - 20 + r, c.name()));
+            }
+        }
+    }  
+
     private DoubleColumn sum(DoubleColumn... columns) {
         int size = columns[0].size();
         DoubleColumn result = DoubleColumn.create("sum", size);
@@ -547,4 +636,18 @@ public class TableTest {
         }
         return result;
     }
+
+    @Test
+    public void testToStringColumnsWithVaryingSizes() {
+        IntColumn col11 = IntColumn.create("col1");
+        IntColumn col12 = IntColumn.create("col2");
+        Table t1 = Table.create("t1", col11, col12);
+        col11.append(1).append(2);
+        col12.append(1);
+        try {
+            assertNotNull(t1.toString());
+        } catch (Exception e) {
+            fail("toString shouldn't throw " + e);
+        }
+    }
 }
diff --git a/core/src/test/java/tech/tablesaw/columns/numbers/NumberFillersTest.java b/core/src/test/java/tech/tablesaw/columns/numbers/NumberFillersTest.java
index f289fc9e..71d2a83b 100644
--- a/core/src/test/java/tech/tablesaw/columns/numbers/NumberFillersTest.java
+++ b/core/src/test/java/tech/tablesaw/columns/numbers/NumberFillersTest.java
@@ -17,6 +17,11 @@ public class NumberFillersTest {
         assertEquals(expected.length, num);
     }
 
+    @Test
+    public void fillWithDouble() {
+        assertContentEquals(create("doubles", new double[5]).fillWith(1.0), 1.0, 1.0, 1.0, 1.0, 1.0);
+    }
+    
     @Test
     public void testFromToBy() {
         assertContentEquals(create("doubles", new double[5]).fillWith(range(1.0, 12.0, 2.5)), 1.0, 3.5, 6.0, 8.5, 11.0);
diff --git a/core/src/test/java/tech/tablesaw/conversion/smile/SmileConverterTest.java b/core/src/test/java/tech/tablesaw/conversion/smile/SmileConverterTest.java
index 890b05f0..3707fbf4 100644
--- a/core/src/test/java/tech/tablesaw/conversion/smile/SmileConverterTest.java
+++ b/core/src/test/java/tech/tablesaw/conversion/smile/SmileConverterTest.java
@@ -37,6 +37,16 @@ public class SmileConverterTest {
         OLS winsModel = new OLS(moneyball.select("W", "RD").smile().numericDataset("RD"));
         assertNotNull(winsModel.toString());
     }
+    
+    @Test
+    public void regressionWithStratifiedSampleTest() throws IOException {
+        Table moneyball = Table.read().csv("../data/baseball.csv");
+        Table[] splits = moneyball.stratifiedSampleSplit(moneyball.stringColumn("Team"), 0.6);
+        Table stratifiedMoneyBall = splits[0];
+        stratifiedMoneyBall.addColumns(stratifiedMoneyBall.numberColumn("RS").subtract(stratifiedMoneyBall.numberColumn("RA")).setName("RD"));
+        OLS winsModel = new OLS(stratifiedMoneyBall.select("W", "RD").smile().numericDataset("RD"));
+        assertNotNull(winsModel.toString());
+    }
 
     @Test
     public void classification() throws IOException {
diff --git a/core/src/test/java/tech/tablesaw/index/DoubleIndexTest.java b/core/src/test/java/tech/tablesaw/index/DoubleIndexTest.java
index b0cb54e9..9682d5e0 100644
--- a/core/src/test/java/tech/tablesaw/index/DoubleIndexTest.java
+++ b/core/src/test/java/tech/tablesaw/index/DoubleIndexTest.java
@@ -50,35 +50,35 @@ public class DoubleIndexTest {
 
     @Test
     public void testGet() {
-        Selection fromCol = table.numberColumn("stop_lat").eval(NumberPredicates.isEqualTo, 30.330425);
+        Selection fromCol = table.numberColumn("stop_lat").eval(NumberPredicates.isEqualTo(30.330425));
         Selection fromIdx = index.get(30.330425);
         assertEquals(fromCol, fromIdx);
     }
 
     @Test
     public void testGTE() {
-        Selection fromCol = table.numberColumn("stop_lat").eval(NumberPredicates.isGreaterThanOrEqualTo, 30.330425);
+        Selection fromCol = table.numberColumn("stop_lat").eval(NumberPredicates.isGreaterThanOrEqualTo(30.330425));
         Selection fromIdx = index.atLeast(30.330425);
         assertEquals(fromCol, fromIdx);
     }
 
     @Test
     public void testLTE() {
-        Selection fromCol = table.numberColumn("stop_lat").eval(NumberPredicates.isLessThanOrEqualTo, 30.330425);
+        Selection fromCol = table.numberColumn("stop_lat").eval(NumberPredicates.isLessThanOrEqualTo(30.330425));
         Selection fromIdx = index.atMost(30.330425);
         assertEquals(fromCol, fromIdx);
     }
 
     @Test
     public void testLT() {
-        Selection fromCol = table.numberColumn("stop_lat").eval(NumberPredicates.isLessThan, 30.330425);
+        Selection fromCol = table.numberColumn("stop_lat").eval(NumberPredicates.isLessThan(30.330425));
         Selection fromIdx = index.lessThan(30.330425);
         assertEquals(fromCol, fromIdx);
     }
 
     @Test
     public void testGT() {
-        Selection fromCol = table.numberColumn("stop_lat").eval(NumberPredicates.isGreaterThan, 30.330425);
+        Selection fromCol = table.numberColumn("stop_lat").eval(NumberPredicates.isGreaterThan(30.330425));
         Selection fromIdx = index.greaterThan(30.330425);
         assertEquals(fromCol, fromIdx);
     }
diff --git a/core/src/test/java/tech/tablesaw/index/IntIndexTest.java b/core/src/test/java/tech/tablesaw/index/IntIndexTest.java
index 84b76c3e..b99267b6 100644
--- a/core/src/test/java/tech/tablesaw/index/IntIndexTest.java
+++ b/core/src/test/java/tech/tablesaw/index/IntIndexTest.java
@@ -51,7 +51,7 @@ public class IntIndexTest {
 
     @Test
     public void testGet() {
-        Selection fromCol = table.numberColumn("approval").eval(NumberPredicates.isEqualTo, 71);
+        Selection fromCol = table.numberColumn("approval").eval(NumberPredicates.isEqualTo(71));
         Selection fromIdx = index.get(71);
         assertEquals(fromCol, fromIdx);
     }
@@ -69,7 +69,7 @@ public class IntIndexTest {
 
     @Test
     public void testGTE() {
-        Selection fromCol = table.numberColumn("approval").eval(NumberPredicates.isGreaterThanOrEqualTo, 71);
+        Selection fromCol = table.numberColumn("approval").eval(NumberPredicates.isGreaterThanOrEqualTo(71));
         Selection fromIdx = index.atLeast(71);
         assertEquals(fromCol, fromIdx);
     }
@@ -85,21 +85,21 @@ public class IntIndexTest {
 
     @Test
     public void testLTE() {
-        Selection fromCol = table.numberColumn("approval").eval(NumberPredicates.isLessThanOrEqualTo, 71);
+        Selection fromCol = table.numberColumn("approval").eval(NumberPredicates.isLessThanOrEqualTo(71));
         Selection fromIdx = index.atMost(71);
         assertEquals(fromCol, fromIdx);
     }
 
     @Test
     public void testLT() {
-        Selection fromCol = table.numberColumn("approval").eval(NumberPredicates.isLessThan, 71);
+        Selection fromCol = table.numberColumn("approval").eval(NumberPredicates.isLessThan(71));
         Selection fromIdx = index.lessThan(71);
         assertEquals(fromCol, fromIdx);
     }
 
     @Test
     public void testGT() {
-        Selection fromCol = table.numberColumn("approval").eval(NumberPredicates.isGreaterThan, 71);
+        Selection fromCol = table.numberColumn("approval").eval(NumberPredicates.isGreaterThan(71));
         Selection fromIdx = index.greaterThan(71);
         assertEquals(fromCol, fromIdx);
     }
diff --git a/core/src/test/java/tech/tablesaw/io/csv/CsvWriterTest.java b/core/src/test/java/tech/tablesaw/io/csv/CsvWriterTest.java
index 0282d27c..1ac52995 100644
--- a/core/src/test/java/tech/tablesaw/io/csv/CsvWriterTest.java
+++ b/core/src/test/java/tech/tablesaw/io/csv/CsvWriterTest.java
@@ -21,7 +21,7 @@ public class CsvWriterTest {
         Table table = Table.create("testTable", colA, colB);
         StringWriter writer = new StringWriter();
         table.write().toWriter(writer, "csv");
-        assertEquals("colA,colB\na,1\nb,2\n", writer.toString());
+        assertEquals("colA,colB\na,1\nb,2\n", writer.toString().replaceAll("\\r\\n", "\n"));
     }
 
 }
diff --git a/core/src/test/java/tech/tablesaw/io/fixed/FixedWidthReaderTest.java b/core/src/test/java/tech/tablesaw/io/fixed/FixedWidthReaderTest.java
index 5f3b2792..9136dfb0 100644
--- a/core/src/test/java/tech/tablesaw/io/fixed/FixedWidthReaderTest.java
+++ b/core/src/test/java/tech/tablesaw/io/fixed/FixedWidthReaderTest.java
@@ -53,7 +53,7 @@ public class FixedWidthReaderTest {
                 .columnTypes(car_types)
                 .columnSpecs(car_fields_specs)
                 .padding('_')
-                .lineEnding("\n")
+                .systemLineEnding()
                 .build()
         );
 
@@ -75,7 +75,7 @@ public class FixedWidthReaderTest {
                 .columnTypes(car_types_with_SKIP)
                 .columnSpecs(car_fields_specs)
                 .padding('_')
-                .lineEnding("\n")
+                .systemLineEnding()
                 .build());
 
         assertEquals(4, table.columnCount());
@@ -92,7 +92,7 @@ public class FixedWidthReaderTest {
                 .columnTypes(car_types_with_SKIP)
                 .columnSpecs(car_fields_specs)
                 .padding('_')
-                .lineEnding("\n")
+                .systemLineEnding()
                 .skipTrailingCharsUntilNewline(true)
                 .build());
 
@@ -110,7 +110,7 @@ public class FixedWidthReaderTest {
                 .header(true)
                 .columnSpecs(car_fields_specs)
                 .padding('_')
-                .lineEnding("\n")
+                .systemLineEnding()
                 .sample(false)
                 .locale(Locale.getDefault())
                 .minimizeColumnSizes()
@@ -129,7 +129,7 @@ public class FixedWidthReaderTest {
                 .header(true)
                 .columnSpecs(car_fields_specs)
                 .padding('_')
-                .lineEnding("\n")
+                .systemLineEnding()
                 .missingValueIndicator("null")
                 .minimizeColumnSizes()
                 .sample(false)
@@ -153,7 +153,7 @@ public class FixedWidthReaderTest {
                 .columnTypes(car_types)
                 .columnSpecs(car_fields_specs)
                 .padding('_')
-                .lineEnding("\n")
+                .systemLineEnding()
                 .skipTrailingCharsUntilNewline(true)
                 .build()
         );
diff --git a/core/src/test/java/tech/tablesaw/joining/DataFrameJoinerTest.java b/core/src/test/java/tech/tablesaw/joining/DataFrameJoinerTest.java
index 88c2c799..50931899 100644
--- a/core/src/test/java/tech/tablesaw/joining/DataFrameJoinerTest.java
+++ b/core/src/test/java/tech/tablesaw/joining/DataFrameJoinerTest.java
@@ -488,6 +488,37 @@ public class DataFrameJoinerTest {
         assertEquals(5, joined.rowCount());
     }
 
+    @Test
+    public void innerJoinSingleColumn() {
+        Table joined = SP500.select("Date").joinOn("Date")
+            .inner(ONE_YEAR.select("Date"));
+        assertEquals(5, joined.rowCount());
+        assertEquals(1, joined.columnCount());
+    }
+
+    @Test
+    public void innerJoinSingleColumnOnRight() {
+        Table joined = SP500.joinOn("Date").inner(ONE_YEAR.select("Date"));
+        assertEquals(5, joined.rowCount());
+        assertEquals(2, joined.columnCount());
+    }
+
+    @Test
+    public void innerJoinMultipleColumnsAllColumnsInJoinKey() {
+        Table table1 = Table.create(
+            SP500.dateColumn("Date"),
+            SP500.dateColumn("Date").copy().setName("Date2")
+        );
+        Table table2 = Table.create(
+            ONE_YEAR.dateColumn("Date"),
+            ONE_YEAR.dateColumn("Date").copy().setName("Date2")
+        );
+
+        Table joined = table1.joinOn("Date", "Date2").inner(table2);
+        assertEquals(5, joined.rowCount());
+        assertEquals(2, joined.columnCount());
+    }
+
     @Test
     public void innerJoinWithBoolean() {
         Table joined = DUPLICATE_COL_NAME_DOGS.joinOn("Good")
@@ -573,6 +604,13 @@ public class DataFrameJoinerTest {
         assertEquals(2, joined.column("Feed").countMissing());
     }
 
+    @Test
+    public void fullOuterJoinColTwoOnlyJoinKeys() {
+        Table joined = ANIMAL_FEED.joinOn("Animal").fullOuter(ANIMAL_NAMES.select("Animal"));
+        assertEquals(2, joined.columnCount());
+        assertEquals(8, joined.rowCount());
+    }
+
     @Test
     public void fullOuterJoinNew() {
         Table joined = ANIMAL_FEED.joinOn("Animal").fullOuter(true, ANIMAL_NAMES);
diff --git a/core/src/test/java/tech/tablesaw/table/TableSliceGroupTest.java b/core/src/test/java/tech/tablesaw/table/TableSliceGroupTest.java
index 0b7685eb..ac1adb0c 100644
--- a/core/src/test/java/tech/tablesaw/table/TableSliceGroupTest.java
+++ b/core/src/test/java/tech/tablesaw/table/TableSliceGroupTest.java
@@ -17,6 +17,7 @@ package tech.tablesaw.table;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import com.google.common.collect.ImmutableListMultimap;
 import java.util.List;
 
 import org.apache.commons.math3.stat.StatUtils;
@@ -87,4 +88,20 @@ public class TableSliceGroupTest {
         List<Table> tables = group.asTableList();
         assertEquals(6, tables.size());
     }
+
+    @Test
+    public void aggregate() {
+        TableSliceGroup group = StandardTableSliceGroup.create(table, table.categoricalColumn("who"));
+        Table aggregated = group.aggregate("approval", exaggerate);
+        assertEquals(aggregated.rowCount(), group.size());
+    }
+
+    @Test
+    public void aggregateWithMultipleColumns() {
+        table.addColumns(table.categoricalColumn("approval").copy().setName("approval2"));
+        TableSliceGroup group = StandardTableSliceGroup.create(table, table.categoricalColumn("who"));
+
+        Table aggregated = group.aggregate(ImmutableListMultimap.of("approval", exaggerate, "approval2", exaggerate));
+        assertEquals(aggregated.rowCount(), group.size());
+    }
 }
\ No newline at end of file
diff --git a/data/dateTimeTestFile.csv b/data/dateTimeTestFile.csv
new file mode 100644
index 00000000..36801267
--- /dev/null
+++ b/data/dateTimeTestFile.csv
@@ -0,0 +1,11 @@
+Time,Value
+2018-04-02T09:26:21.000,24336
+2018-04-02T09:29:21.000,24342
+2018-04-02T09:31:21.000,24355
+2018-04-02T09:41:21.000,24381
+2018-04-02T09:50:21.000,24391
+2018-04-02T09:55:21.000,24402
+2018-04-02T09:57:21.000,24412
+2018-04-02T10:01:21.000,24407
+2018-04-02T10:04:21.000,24401
+2018-04-02T10:05:21.000,24351
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/DotPlotExample.java b/jsplot/src/test/java/tech/tablesaw/examples/DotPlotExample.java
new file mode 100644
index 00000000..90dac2fe
--- /dev/null
+++ b/jsplot/src/test/java/tech/tablesaw/examples/DotPlotExample.java
@@ -0,0 +1,70 @@
+package tech.tablesaw.examples;
+
+import tech.tablesaw.aggregate.AggregateFunctions;
+import tech.tablesaw.api.CategoricalColumn;
+import tech.tablesaw.api.IntColumn;
+import tech.tablesaw.api.NumberColumn;
+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.components.Marker;
+import tech.tablesaw.plotly.components.Symbol;
+import tech.tablesaw.plotly.traces.ScatterTrace;
+
+public class DotPlotExample {
+
+    public static void main(String[] args) throws Exception {
+
+        Table bush = Table.read().csv("../data/bush.csv");
+
+        NumberColumn<?> x = bush.nCol("approval");
+        CategoricalColumn<?> y = bush.stringColumn("who");
+
+        Layout layout = Layout.builder()
+                .title("Approval ratings by agency")
+                .build();
+
+        ScatterTrace trace = ScatterTrace.builder(x, y)
+                .mode(ScatterTrace.Mode.MARKERS)
+                .build();
+        Plot.show(new Figure(layout, trace));
+
+
+        // A more complex example involving two traces
+        IntColumn year = bush.dateColumn("date").year();
+        year.setName("year");
+        bush.addColumns(year);
+        bush.dropWhere(bush.intColumn("year").isIn((Number)2001, (Number) 2002));
+        Table summary = bush.summarize("approval", AggregateFunctions.mean).by("who", "year");
+        
+        Layout layout2 = Layout.builder()
+                .title("Mean approval ratings by agency and year for 2001 and 2002")
+                .build();
+
+        Table year1 = summary.where(summary.intColumn("year").isEqualTo(2001));
+        Table year2 = summary.where(summary.intColumn("year").isEqualTo(2002));
+        ScatterTrace trace2 = ScatterTrace.builder(year1.nCol("Mean [approval]"), year1.stringColumn("who"))
+                .name("2001")
+                .mode(ScatterTrace.Mode.MARKERS)
+                .marker(Marker.builder()
+                        .symbol(Symbol.DIAMOND)
+                        .color("red")
+                        .size(10)
+                        .build())
+                .build();
+
+        ScatterTrace trace3 = ScatterTrace.builder(year2.nCol("Mean [approval]"), year2.stringColumn("who"))
+                .name("2002")
+                .mode(ScatterTrace.Mode.MARKERS)
+                .marker(Marker.builder()
+                        .symbol(Symbol.STAR)
+                        .size(10)
+                        .color("blue")
+                        .build())
+                .build();
+
+        Plot.show(new Figure(layout2, trace2, trace3));
+
+    }
+}
diff --git a/jsplot/src/test/java/tech/tablesaw/examples/HoverBroadcastExample.java b/jsplot/src/test/java/tech/tablesaw/examples/HoverBroadcastExample.java
new file mode 100644
index 00000000..20ae6989
--- /dev/null
+++ b/jsplot/src/test/java/tech/tablesaw/examples/HoverBroadcastExample.java
@@ -0,0 +1,71 @@
+package tech.tablesaw.examples;
+
+import java.util.Random;
+import java.time.LocalDateTime;
+
+import tech.tablesaw.api.DateTimeColumn;
+import tech.tablesaw.api.DoubleColumn;
+import tech.tablesaw.api.IntColumn;
+import tech.tablesaw.plotly.Plot;
+import tech.tablesaw.plotly.traces.ScatterTrace;
+import tech.tablesaw.plotly.components.Axis;
+import tech.tablesaw.plotly.components.Layout;
+import tech.tablesaw.plotly.components.Figure;
+import tech.tablesaw.plotly.event.HoverBroadcastBody;
+import tech.tablesaw.plotly.event.HoverEventHandler;
+
+public class HoverBroadcastExample {
+
+    public static void main(String[] args) throws Exception {
+        int seriesLen = 50;
+        Random rng = new Random();
+        Double[] prices = new Double[seriesLen];
+        int[] volumes = new int[seriesLen];
+        LocalDateTime startDT = LocalDateTime.of(2019, 1, 1, 9, 30, 0);
+        LocalDateTime[] times = new LocalDateTime[seriesLen];
+
+        HoverBroadcastBody hbb = HoverBroadcastBody.builder()
+            .subPlots(new String[] { "xy", "xy2" })
+            .numTraces(2)
+            .build();
+
+        HoverEventHandler heh = HoverEventHandler.builder().body(hbb).build();
+
+        for (int i = 0; i < seriesLen; i++) {
+            prices[i] = 25.0 + rng.nextDouble();
+            volumes[i] = rng.nextInt(10000);
+            times[i] = startDT.plusMinutes(i);
+        }
+
+        DateTimeColumn x = DateTimeColumn.create("time", times);
+        DoubleColumn y2 = DoubleColumn.create("price", prices);
+        IntColumn y1 = IntColumn.create("volume", volumes);
+
+        ScatterTrace trace0 = ScatterTrace.builder(x, y2)
+            .showLegend(true)
+            .name("Price")
+            .mode(ScatterTrace.Mode.LINE)
+            .yAxis(ScatterTrace.YAxis.Y2)
+            .build();
+
+        ScatterTrace trace1 = ScatterTrace.builder(x, y1)
+            .showLegend(true)
+            .name("Volume")
+            .mode(ScatterTrace.Mode.LINE)
+            .yAxis(ScatterTrace.YAxis.Y)
+            .build();
+
+        Layout layout = Layout.builder("Stock Data", "Time", "Price")
+            .yAxis(Axis.builder()
+                    .title("volume")
+                    .domain(0f, 0.25f)
+                    .build())
+            .yAxis2(Axis.builder()
+                    .title("price")
+                    .domain(0.35f, 1.0f)
+                    .build())
+            .build();
+
+        Plot.show(new Figure(layout, heh, trace0, trace1));
+    }
+}
diff --git a/jsplot/src/test/java/tech/tablesaw/examples/QQPlotExample.java b/jsplot/src/test/java/tech/tablesaw/examples/QQPlotExample.java
new file mode 100644
index 00000000..16d41fcd
--- /dev/null
+++ b/jsplot/src/test/java/tech/tablesaw/examples/QQPlotExample.java
@@ -0,0 +1,42 @@
+/*
+ * 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 org.apache.commons.math3.distribution.NormalDistribution;
+import tech.tablesaw.api.Table;
+import tech.tablesaw.plotly.Plot;
+import tech.tablesaw.plotly.api.QQPlot;
+
+/**
+ *  Illustrates how to create a quantile plot for visualizing a distribution
+ */
+public class QQPlotExample {
+
+    public static void main(String[] args) throws Exception {
+        Table baseball = Table.read().csv("../data/baseball.csv");
+        Plot.show(QQPlot.create("batting averages and On-base percent", baseball, "BA", "SLG"));
+
+        // example with different sized arrays;
+        double[] first = new NormalDistribution().sample(100);
+        double[] second = new NormalDistribution().sample(200);
+        Plot.show(QQPlot.create("Test of different sized arrays", "short array", first, "long array", second));
+
+        // example with different sized arrays;
+        first = new NormalDistribution().sample(20000);
+        second = new NormalDistribution().sample(19990);
+        Plot.show(QQPlot.create("Test of different sized arrays", "long array", first, "short array", second));
+
+    }
+}
\ No newline at end of file
diff --git a/jsplot/src/test/java/tech/tablesaw/examples/QuantileExample.java b/jsplot/src/test/java/tech/tablesaw/examples/QuantileExample.java
index 21dc3981..2cb904a6 100644
--- a/jsplot/src/test/java/tech/tablesaw/examples/QuantileExample.java
+++ b/jsplot/src/test/java/tech/tablesaw/examples/QuantileExample.java
@@ -14,34 +14,17 @@
 
 package tech.tablesaw.examples;
 
-import tech.tablesaw.api.NumberColumn;
 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.ScatterTrace;
+import tech.tablesaw.plotly.api.QuantilePlot;
 
 /**
- *
+ *  Illustrates how to create a quantile plot for visualizing a distribution
  */
 public class QuantileExample {
 
     public static void main(String[] args) throws Exception {
         Table baseball = Table.read().csv("../data/baseball.csv");
-        NumberColumn<?> xCol = baseball.nCol("BA");
-
-        double[] x = new double[xCol.size()];
-
-        for (int i = 0; i < x.length; i++) {
-            x[i] = i / (float) x.length;
-        }
-
-        NumberColumn<?> copy = xCol.copy();
-        copy.sortAscending();
-
-        ScatterTrace trace = ScatterTrace.builder(x, copy.asDoubleArray()).build();
-
-        Layout layout = Layout.builder().title("Distribution of team batting averages").build();
-        Plot.show(new Figure(layout, trace));
+        Plot.show(QuantilePlot.create("Distribution of team batting averages", baseball, "BA"));
     }
 }
\ No newline at end of file
diff --git a/jsplot/src/test/java/tech/tablesaw/examples/ScattterPlotMatrixExample.java b/jsplot/src/test/java/tech/tablesaw/examples/ScattterPlotMatrixExample.java
new file mode 100644
index 00000000..9a762ffb
--- /dev/null
+++ b/jsplot/src/test/java/tech/tablesaw/examples/ScattterPlotMatrixExample.java
@@ -0,0 +1,76 @@
+/*
+ * 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 tech.tablesaw.api.NumericColumn;
+import tech.tablesaw.api.Table;
+import tech.tablesaw.plotly.Plot;
+import tech.tablesaw.plotly.components.Figure;
+import tech.tablesaw.plotly.components.Grid;
+import tech.tablesaw.plotly.components.Layout;
+import tech.tablesaw.plotly.components.Marker;
+import tech.tablesaw.plotly.traces.ScatterTrace;
+import tech.tablesaw.plotly.traces.Trace;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Renders a scatter plot matrix for the first 6 numeric columns in the tornado dataset
+ */
+public class ScattterPlotMatrixExample {
+
+    public static void main(String[] args) throws Exception {
+        Table table = Table.read().csv("../data/tornadoes_1950-2014.csv").sampleN(500);
+
+        List<NumericColumn<?>> columns = table.numericColumns().subList(0, 6);
+        List<Trace> traceList = new ArrayList<>();
+        int count = 1;
+        for (int i = 0; i < columns.size(); i++) {
+            for (NumericColumn<?> column : columns) {
+                Trace t = ScatterTrace.builder(
+                        column.asDoubleArray(),
+                        columns.get(i).asDoubleArray())
+                        .xAxis("x" + count)
+                        .yAxis("y" + count)
+                        .name(columns.get(i).name() + " x " + column.name())
+                        .marker(Marker.builder()
+                                .size(3)
+                                .opacity(.5)
+                                .build())
+                        .build();
+
+                traceList.add(t);
+                count++;
+            }
+        }
+        Trace[] traces = traceList.toArray(new Trace[0]);
+
+        Grid grid = Grid.builder()
+                .columns(columns.size())
+                .rows(columns.size())
+                .pattern(Grid.Pattern.INDEPENDENT)
+                .xSide(Grid.XSide.BOTTOM)
+                .build();
+
+        Layout layout = Layout.builder()
+                .title("Scatter Plot Matrix")
+                .width(1100)
+                .height(1100)
+                .grid(grid)
+                .build();
+
+        Plot.show(new Figure(layout, traces));
+    }
+}
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..ca7383f2
--- /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 tech.tablesaw.api.Table;
+import tech.tablesaw.plotly.Plot;
+import tech.tablesaw.plotly.components.Figure;
+import tech.tablesaw.plotly.components.Grid;
+import tech.tablesaw.plotly.components.Layout;
+import tech.tablesaw.plotly.traces.HistogramTrace;
+import tech.tablesaw.plotly.traces.Trace;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Renders a 2 (row) by 4 (col) grid of histograms, one for each of the 8 numeric columns in the tornadoes dataset
+ */
+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/examples/TimeSeriesVisualizations.java b/jsplot/src/test/java/tech/tablesaw/examples/TimeSeriesVisualizations.java
index ab10a4df..846bc29a 100644
--- a/jsplot/src/test/java/tech/tablesaw/examples/TimeSeriesVisualizations.java
+++ b/jsplot/src/test/java/tech/tablesaw/examples/TimeSeriesVisualizations.java
@@ -44,8 +44,14 @@ public class TimeSeriesVisualizations {
 
         Table priceTable = Table.read().csv("../data/ohlcdata.csv");
         priceTable.numberColumn("Volume").setPrintFormatter(NumberColumnFormatter.intsWithGrouping());
-        System.out.println(priceTable.first(5));
         Plot.show(OHLCPlot.create("Prices", priceTable, "date", "open", "high", "low", "close"));
         Plot.show(CandlestickPlot.create("Prices", priceTable, "date", "open", "high", "low", "close"));
+
+        // using a datetime column
+        Table dateTable = Table.read().csv("../data/dateTimeTestFile.csv");
+        Plot.show(TimeSeriesPlot.create("Value over time", "time", dateTable.dateTimeColumn("Time"), "values", dateTable.numberColumn("Value")));
+
+        // using a datetime column2
+        Plot.show(TimeSeriesPlot.createDateTimeSeries("Value over time", dateTable, "Time", "Value"));
     }
 }
diff --git a/jsplot/src/test/java/tech/tablesaw/examples/TukeyMeanDistributionPlotExample.java b/jsplot/src/test/java/tech/tablesaw/examples/TukeyMeanDistributionPlotExample.java
new file mode 100644
index 00000000..619d78a5
--- /dev/null
+++ b/jsplot/src/test/java/tech/tablesaw/examples/TukeyMeanDistributionPlotExample.java
@@ -0,0 +1,40 @@
+/*
+ * 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 org.apache.commons.math3.distribution.NormalDistribution;
+import tech.tablesaw.api.Table;
+import tech.tablesaw.plotly.Plot;
+import tech.tablesaw.plotly.api.TukeyMeanDifferencePlot;
+
+/**
+ *  Illustrates how to create a quantile plot for visualizing a distribution
+ */
+public class TukeyMeanDistributionPlotExample {
+
+    public static void main(String[] args) throws Exception {
+        Table baseball = Table.read().csv("../data/baseball.csv");
+        Table nl = baseball.where(baseball.stringColumn("league").isEqualTo("NL"));
+        Table al = baseball.where(baseball.stringColumn("league").isEqualTo("AL"));
+        Plot.show(TukeyMeanDifferencePlot.create("Wins NL vs AL", "wins",
+                nl.intColumn("W").asDoubleArray(),
+                al.intColumn("W").asDoubleArray()));
+
+        // example with difference sized arrays;
+        double[] first = new NormalDistribution().sample(100);
+        double[] second = new NormalDistribution().sample(200);
+        Plot.show(TukeyMeanDifferencePlot.create("Test of different sized arrays", "random data", first, second));
+    }
+}
\ No newline at end of file
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);
+    }
+}