Repository: jtablesaw/tablesaw
Pull request #482 (2019-03-07 12:15:05)

Issue 479 template customization
property.

Thanks for contributing.

- [x] Tick to sign-off your agreement to the [Developer Certificate of Origin (DCO) 1.1](https://developercertificate.org)

## Description

Modified the template to support setting location of plotly js library, and Page and PageBuilder accordingly.

## Testing

There are tests for both cases, the default and custom cases.

hallvard: The test succeeds locally when run using mvn clean test. jsplot defines the following dependency on junit:
```
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>
```
benmccann: Not anymore: https://github.com/jtablesaw/tablesaw/blob/master/jsplot/pom.xml#L65

We just upgraded to junit 5. Can you rebase against master? 
hallvard: Hm, for some reason I don't get the changes, I'm told my upstream/master branch pointing to https://github.com/jtablesaw/tablesaw.git is up-to-date and the last commit I see is "Use static imports for junit asserts" 
hallvard: Strange, when I turned off VPN I suddenly could fetch the latest changes.
hallvard: I use Assertions.assertTrue, isn't that good enough?
benmccann: Perhaps it doesn't have great support for junit 5 yet. Everywhere else we statically import `assertTrue` and I'm guessing that makes it happier. Can you try that?

Patch:
diff --git a/README.md b/README.md
index 9845073a..f02f39aa 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ __Tablesaw__ is Java for data science. It includes a dataframe and a visualizati
 ### Tablesaw features
 
 #### Data processing & transformation
-* Import data from RDBMS, CSV, JSON, HTML, or Fixed Width text files, whether they are local or remote (http, S3, etc.)
+* Import data from RDBMS, Excel, CSV, JSON, HTML, or Fixed Width text files, whether they are local or remote (http, S3, etc.)
 * Export data to CSV, JSON, HTML or Fixed Width files. 
 * Combine tables by appending or joining
 * Add and remove columns or rows
@@ -43,12 +43,12 @@ Add tablesaw-core and tablesaw-jsplot to your project:
 <dependency>
     <groupId>tech.tablesaw</groupId>
     <artifactId>tablesaw-core</artifactId>
-    <version>0.30.4</version>
+    <version>0.31.0</version>
 </dependency>
 <dependency>
     <groupId>tech.tablesaw</groupId>
     <artifactId>tablesaw-jsplot</artifactId>
-    <version>0.30.4</version>
+    <version>0.31.0</version>
 </dependency>
 ```
 
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 dd195132..3b02687f 100644
--- a/core/src/main/java/tech/tablesaw/io/csv/CsvReadOptions.java
+++ b/core/src/main/java/tech/tablesaw/io/csv/CsvReadOptions.java
@@ -30,6 +30,7 @@ public class CsvReadOptions extends ReadOptions {
     private final Character separator;
     private final String lineEnding;
     private final Integer maxNumberOfColumns;
+    private final Character commentPrefix;
 
     private CsvReadOptions(CsvReadOptions.Builder builder) {
 	super(builder);
@@ -38,6 +39,7 @@ public class CsvReadOptions extends ReadOptions {
         separator = builder.separator;
         lineEnding = builder.lineEnding;
         maxNumberOfColumns = builder.maxNumberOfColumns;
+        commentPrefix = builder.commentPrefix;
     }
 
     public static Builder builder(File file) {
@@ -139,13 +141,18 @@ public class CsvReadOptions extends ReadOptions {
         return maxNumberOfColumns;
     }
 
+    public Character commentPrefix() {
+        return commentPrefix;
+    }
+
     public static class Builder extends ReadOptions.Builder {
 
         private Character separator = ',';
         private String lineEnding;
         private ColumnType[] columnTypes;
         private Integer maxNumberOfColumns = 10_000;
-
+        private Character commentPrefix;
+        
         public Builder(File file) {
             super(file);
         }
@@ -244,6 +251,11 @@ public class CsvReadOptions extends ReadOptions {
             return this;
         }
 
+        public Builder commentPrefix(char commentPrefix) {
+            this.commentPrefix = commentPrefix;
+            return this;
+        }
+
         public CsvReadOptions build() {
             return new CsvReadOptions(this);
         }
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 ae1b0e16..5063febd 100644
--- a/core/src/main/java/tech/tablesaw/io/csv/CsvReader.java
+++ b/core/src/main/java/tech/tablesaw/io/csv/CsvReader.java
@@ -19,13 +19,15 @@ import com.univocity.parsers.common.AbstractParser;
 import com.univocity.parsers.csv.CsvFormat;
 import com.univocity.parsers.csv.CsvParser;
 import com.univocity.parsers.csv.CsvParserSettings;
-import org.apache.commons.lang3.tuple.Pair;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.api.Table;
 import tech.tablesaw.io.FileReader;
 import tech.tablesaw.io.TableBuildingUtils;
 
 import javax.annotation.concurrent.Immutable;
+
+import org.apache.commons.math3.util.Pair;
+
 import java.io.IOException;
 import java.io.Reader;
 import java.util.List;
@@ -67,7 +69,7 @@ public class CsvReader extends FileReader {
             types = detectColumnTypes(reader, options);
         }
 
-        return Pair.of(TableBuildingUtils.createReader(options, bytesCache), types);
+        return Pair.create(TableBuildingUtils.createReader(options, bytesCache), types);
     }
 
     public Table read(CsvReadOptions options) throws IOException {
@@ -76,8 +78,8 @@ public class CsvReader extends FileReader {
 
     private Table read(CsvReadOptions options, boolean headerOnly) throws IOException {
         Pair<Reader, ColumnType[]> pair = getReaderAndColumnTypes(options);
-        Reader reader = pair.getLeft();
-        ColumnType[] types = pair.getRight();
+        Reader reader = pair.getKey();
+        ColumnType[] types = pair.getValue();
 
         AbstractParser<?> parser = csvParser(options);
 
@@ -161,6 +163,9 @@ public class CsvReader extends FileReader {
         if (options.lineEnding() != null) {
             format.setLineSeparator(options.lineEnding());
         }
+        if(options.commentPrefix() != null) {
+            format.setComment(options.commentPrefix());
+        }
         return format;
     }
 
diff --git a/core/src/main/java/tech/tablesaw/io/fixed/FixedWidthReader.java b/core/src/main/java/tech/tablesaw/io/fixed/FixedWidthReader.java
index deedd606..59d2c66a 100644
--- a/core/src/main/java/tech/tablesaw/io/fixed/FixedWidthReader.java
+++ b/core/src/main/java/tech/tablesaw/io/fixed/FixedWidthReader.java
@@ -19,13 +19,15 @@ import com.univocity.parsers.common.AbstractParser;
 import com.univocity.parsers.fixed.FixedWidthFormat;
 import com.univocity.parsers.fixed.FixedWidthParser;
 import com.univocity.parsers.fixed.FixedWidthParserSettings;
-import org.apache.commons.lang3.tuple.Pair;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.api.Table;
 import tech.tablesaw.io.FileReader;
 import tech.tablesaw.io.TableBuildingUtils;
 
 import javax.annotation.concurrent.Immutable;
+
+import org.apache.commons.math3.util.Pair;
+
 import java.io.IOException;
 import java.io.Reader;
 import java.util.List;
@@ -67,7 +69,7 @@ public class FixedWidthReader extends FileReader {
             types = detectColumnTypes(reader, options);
         }
 
-        return Pair.of(TableBuildingUtils.createReader(options, bytesCache), types);
+        return Pair.create(TableBuildingUtils.createReader(options, bytesCache), types);
     }
 
     public Table read(FixedWidthReadOptions options) throws IOException {
@@ -76,8 +78,8 @@ public class FixedWidthReader extends FileReader {
 
     private Table read(FixedWidthReadOptions options, boolean headerOnly) throws IOException {
         Pair<Reader, ColumnType[]> pair = getReaderAndColumnTypes(options);
-        Reader reader = pair.getLeft();
-        ColumnType[] types = pair.getRight();
+        Reader reader = pair.getKey();
+        ColumnType[] types = pair.getValue();
 
         FixedWidthParser parser = fixedWidthParser(options);
 
diff --git a/data/with_comments.csv b/data/with_comments.csv
new file mode 100644
index 00000000..45330b85
--- /dev/null
+++ b/data/with_comments.csv
@@ -0,0 +1,11 @@
+#comment1
+#comment2
+Products,Sales,Market_Share
+# comment3
+a,5500,3
+# comment,with,some,rows
+#comment5
+b,12200,4
+c,60000,33
+#comment,with,rows
+#footer comment
\ No newline at end of file
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/api/AreaPlot.java b/jsplot/src/main/java/tech/tablesaw/plotly/api/AreaPlot.java
index adbab843..dfb3f120 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/api/AreaPlot.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/api/AreaPlot.java
@@ -1,7 +1,6 @@
 package tech.tablesaw.plotly.api;
 
 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;
@@ -11,14 +10,10 @@ import java.util.List;
 
 public class AreaPlot {
 
-    private static final int HEIGHT = 600;
-    private static final int WIDTH = 800;
-
     public static Figure create(String title, Table table, String xCol, String yCol, String groupCol) {
-
         TableSliceGroup tables = table.splitOn(table.categoricalColumn(groupCol));
 
-        Layout layout = standardLayout(title, xCol, yCol).build();
+        Layout layout = Layout.builder(title, xCol, yCol).showLegend(true).build();
 
         ScatterTrace[] traces  = new ScatterTrace[tables.size()];
         for (int i = 0; i < tables.size(); i++) {
@@ -35,24 +30,8 @@ public class AreaPlot {
         return new Figure(layout, traces);
     }
 
-    private static Layout.LayoutBuilder standardLayout(String title, String xCol, String yCol) {
-        return Layout.builder()
-                    .title(title)
-                    .height(HEIGHT)
-                    .width(WIDTH)
-                    .showLegend(true)
-                    .xAxis(Axis.builder()
-                            .title(xCol)
-                            .build())
-                    .yAxis(Axis.builder()
-                            .title(yCol)
-                            .build());
-    }
-
     public static Figure create(String title, Table table, String xCol, String yCol) {
-
-        Layout layout = standardLayout(title, xCol, yCol).build();
-
+        Layout layout = Layout.builder(title, xCol, yCol).showLegend(true).build();
         ScatterTrace trace = ScatterTrace.builder(
                 table.numberColumn(xCol),
                 table.numberColumn(yCol))
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/api/BubblePlot.java b/jsplot/src/main/java/tech/tablesaw/plotly/api/BubblePlot.java
index da0356ba..9de6f444 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/api/BubblePlot.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/api/BubblePlot.java
@@ -1,7 +1,6 @@
 package tech.tablesaw.plotly.api;
 
 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.components.Marker;
@@ -12,24 +11,10 @@ import java.util.List;
 
 public class BubblePlot {
 
-    private static final int HEIGHT = 600;
-    private static final int WIDTH = 800;
-
     public static Figure create(String title, Table table, String xCol, String yCol, String sizeColumn, String groupCol) {
 
         TableSliceGroup tables = table.splitOn(table.categoricalColumn(groupCol));
-        Layout layout = Layout.builder()
-                .title(title)
-                .height(HEIGHT)
-                .width(WIDTH)
-                .showLegend(true)
-                .xAxis(Axis.builder()
-                        .title(xCol)
-                        .build())
-                .yAxis(Axis.builder()
-                        .title(yCol)
-                        .build())
-                .build();
+        Layout layout = Layout.builder(title, xCol, yCol).showLegend(true).build();
 
         ScatterTrace[] traces  = new ScatterTrace[tables.size()];
         for (int i = 0; i < tables.size(); i++) {
@@ -52,18 +37,7 @@ public class BubblePlot {
     }
 
     public static Figure create(String title, Table table, String xCol, String yCol, String sizeColumn) {
-
-        Layout layout = Layout.builder()
-                .title(title)
-                .height(HEIGHT)
-                .width(WIDTH)
-                .xAxis(Axis.builder()
-                        .title(xCol)
-                        .build())
-                .yAxis(Axis.builder()
-                        .title(yCol)
-                        .build())
-                .build();
+        Layout layout = Layout.builder(title, xCol, yCol).build();
 
         Marker marker = Marker.builder()
                 .size(table.numberColumn(sizeColumn))
@@ -79,19 +53,7 @@ public class BubblePlot {
     }
 
     public static Figure create(String title, String xTitle, double[] xCol, String yTitle, double[] yCol) {
-
-        Layout layout = Layout.builder()
-                .title(title)
-                .height(HEIGHT)
-                .width(WIDTH)
-                .xAxis(Axis.builder()
-                        .title(xTitle)
-                        .build())
-                .yAxis(Axis.builder()
-                        .title(yTitle)
-                        .build())
-                .build();
-
+        Layout layout = Layout.builder(title, xTitle, yTitle).build();
         ScatterTrace trace = ScatterTrace.builder(xCol, yCol).build();
         return new Figure(layout, trace);
     }
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/api/CandlestickPlot.java b/jsplot/src/main/java/tech/tablesaw/plotly/api/CandlestickPlot.java
index 2a66ca52..e492a084 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/api/CandlestickPlot.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/api/CandlestickPlot.java
@@ -1,27 +1,14 @@
 package tech.tablesaw.plotly.api;
 
 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;
 
 public class CandlestickPlot {
 
-    private static final int HEIGHT = 600;
-    private static final int WIDTH = 800;
-
     public static Figure create(String title, Table table, String xCol, String openCol, String highCol, String lowCol, String closeCol) {
-
-        Layout layout = Layout.builder()
-                .title(title)
-                .height(HEIGHT)
-                .width(WIDTH)
-                .xAxis(Axis.builder()
-                        .title(xCol)
-                        .build())
-                .build();
-
+        Layout layout = Layout.builder(title, xCol).build();
         ScatterTrace trace = ScatterTrace.builder(
                 table.dateColumn(xCol),
                 table.numberColumn(openCol),
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/api/Heatmap.java b/jsplot/src/main/java/tech/tablesaw/plotly/api/Heatmap.java
index 69e3e137..e00ccb66 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/api/Heatmap.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/api/Heatmap.java
@@ -10,17 +10,11 @@ import tech.tablesaw.util.DoubleArrays;
 import java.util.List;
 
 public class Heatmap {
-    private static final int HEIGHT = 600;
-    private static final int WIDTH = 800;
 
     private Heatmap() {}
 
     public static Figure create(String title, Table table, String categoryCol1, String categoryCol2) {
-        Layout layout = Layout.builder()
-                .title(title)
-                .height(HEIGHT)
-                .width(WIDTH)
-                .build();
+        Layout layout = Layout.builder(title).build();
 
         Table counts = table.xTabCounts(categoryCol1, categoryCol2);
         counts = counts.dropRows(counts.rowCount() - 1);
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/api/Histogram.java b/jsplot/src/main/java/tech/tablesaw/plotly/api/Histogram.java
index 3993701e..dbfe8715 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/api/Histogram.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/api/Histogram.java
@@ -8,9 +8,6 @@ import tech.tablesaw.plotly.traces.HistogramTrace;
 
 public class Histogram {
 
-    private static final int HEIGHT = 600;
-    private static final int WIDTH = 800;
-
     public static Figure create(String title, NumberColumn<?> data) {
         return create(title, data.asDoubleArray());
     }
@@ -21,14 +18,7 @@ public class Histogram {
 
     public static Figure create(String title, double[] data) {
         HistogramTrace trace = HistogramTrace.builder(data).build();
-        return new Figure(standardLayout(title), trace);
+        return new Figure(Layout.builder(title).build(), trace);
     }
 
-    private static Layout standardLayout(String title) {
-        return Layout.builder()
-                .title(title)
-                .height(HEIGHT)
-                .width(WIDTH)
-                .build();
-    }
 }
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/api/Histogram2D.java b/jsplot/src/main/java/tech/tablesaw/plotly/api/Histogram2D.java
index 8c1d0365..bee71029 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/api/Histogram2D.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/api/Histogram2D.java
@@ -1,31 +1,17 @@
 package tech.tablesaw.plotly.api;
 
 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.Histogram2DTrace;
 
 public class Histogram2D {
 
-    private static final int HEIGHT = 600;
-    private static final int WIDTH = 800;
-
     public static Figure create(String title, Table table, String xCol, String yCol) {
-
         Histogram2DTrace trace = Histogram2DTrace.builder(table.numberColumn(xCol), table.numberColumn(yCol))
                 .build();
 
-        return new Figure(standardLayout(title, xCol, yCol), trace);
+        return new Figure(Layout.builder(title, xCol, yCol).build(), trace);
     }
 
-    private static Layout standardLayout(String title, String xCol, String yCol) {
-        return Layout.builder()
-                .title(title)
-                .height(HEIGHT)
-                .width(WIDTH)
-                .xAxis(Axis.builder().title(xCol).build())
-                .yAxis(Axis.builder().title(yCol).build())
-                .build();
-    }
 }
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/api/LinePlot.java b/jsplot/src/main/java/tech/tablesaw/plotly/api/LinePlot.java
index bfe7ce24..dd34c2da 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/api/LinePlot.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/api/LinePlot.java
@@ -1,7 +1,6 @@
 package tech.tablesaw.plotly.api;
 
 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;
@@ -11,25 +10,11 @@ import java.util.List;
 
 public class LinePlot {
 
-    private static final int HEIGHT = 600;
-    private static final int WIDTH = 800;
-
     public static Figure create(String title, Table table, String xCol, String yCol, String groupCol) {
 
         TableSliceGroup tables = table.splitOn(table.categoricalColumn(groupCol));
 
-        Layout layout = Layout.builder()
-                .title(title)
-                .height(HEIGHT)
-                .width(WIDTH)
-                .showLegend(true)
-                .xAxis(Axis.builder()
-                        .title(xCol)
-                        .build())
-                .yAxis(Axis.builder()
-                        .title(yCol)
-                        .build())
-                .build();
+        Layout layout = Layout.builder(title, xCol, yCol).showLegend(true).build();
 
         ScatterTrace[] traces  = new ScatterTrace[tables.size()];
         for (int i = 0; i < tables.size(); i++) {
@@ -46,19 +31,7 @@ public class LinePlot {
     }
 
     public static Figure create(String title, Table table, String xCol, String yCol) {
-
-        Layout layout = Layout.builder()
-                .title(title)
-                .height(HEIGHT)
-                .width(WIDTH)
-                .xAxis(Axis.builder()
-                        .title(xCol)
-                        .build())
-                .yAxis(Axis.builder()
-                        .title(yCol)
-                        .build())
-                .build();
-
+        Layout layout = Layout.builder(title, xCol, yCol).build();
         ScatterTrace trace = ScatterTrace.builder(
                 table.numberColumn(xCol),
                 table.numberColumn(yCol))
@@ -68,19 +41,7 @@ public class LinePlot {
     }
 
     public static Figure create(String title, String xTitle, double[] xCol, String yTitle, double[] yCol) {
-
-        Layout layout = Layout.builder()
-                .title(title)
-                .height(HEIGHT)
-                .width(WIDTH)
-                .xAxis(Axis.builder()
-                        .title(xTitle)
-                        .build())
-                .yAxis(Axis.builder()
-                        .title(yTitle)
-                        .build())
-                .build();
-
+        Layout layout = Layout.builder(title, xTitle, yTitle).build();
         ScatterTrace trace = ScatterTrace.builder(xCol, yCol)
                 .mode(ScatterTrace.Mode.LINE)
                 .build();
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/api/OHLCPlot.java b/jsplot/src/main/java/tech/tablesaw/plotly/api/OHLCPlot.java
index 9a595ebd..e367422a 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/api/OHLCPlot.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/api/OHLCPlot.java
@@ -1,27 +1,14 @@
 package tech.tablesaw.plotly.api;
 
 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;
 
 public class OHLCPlot {
 
-    private static final int HEIGHT = 600;
-    private static final int WIDTH = 800;
-
     public static Figure create(String title, Table table, String xCol, String openCol, String highCol, String lowCol, String closeCol) {
-
-        Layout layout = Layout.builder()
-                .title(title)
-                .height(HEIGHT)
-                .width(WIDTH)
-                .xAxis(Axis.builder()
-                        .title(xCol)
-                        .build())
-                .build();
-
+        Layout layout = Layout.builder(title, xCol).build();
         ScatterTrace trace = ScatterTrace.builder(
                 table.dateColumn(xCol),
                 table.numberColumn(openCol),
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/api/PiePlot.java b/jsplot/src/main/java/tech/tablesaw/plotly/api/PiePlot.java
index c7582825..ed58c367 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/api/PiePlot.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/api/PiePlot.java
@@ -7,16 +7,9 @@ import tech.tablesaw.plotly.traces.PieTrace;
 
 public class PiePlot {
 
-    private static final int HEIGHT = 600;
-    private static final int WIDTH = 800;
-
     public static Figure create(String title, Table table, String groupColName, String numberColName) {
 
-        Layout layout = Layout.builder()
-                .title(title)
-                .height(HEIGHT)
-                .width(WIDTH)
-                .build();
+        Layout layout = Layout.builder(title).build();
 
         PieTrace trace = PieTrace.builder(
                 table.column(groupColName),
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/api/ScatterPlot.java b/jsplot/src/main/java/tech/tablesaw/plotly/api/ScatterPlot.java
index af1f32b2..ffa5fbac 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/api/ScatterPlot.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/api/ScatterPlot.java
@@ -1,7 +1,6 @@
 package tech.tablesaw.plotly.api;
 
 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.components.Marker;
@@ -12,26 +11,13 @@ import java.util.List;
 
 public class ScatterPlot {
 
-    private static final int HEIGHT = 600;
-    private static final int WIDTH = 800;
     private static final double OPACITY = .75;
 
     public static Figure create(String title, Table table, String xCol, String yCol, String groupCol) {
 
         TableSliceGroup tables = table.splitOn(table.categoricalColumn(groupCol));
 
-        Layout layout = Layout.builder()
-                .title(title)
-                .height(HEIGHT)
-                .width(WIDTH)
-                .showLegend(true)
-                .xAxis(Axis.builder()
-                        .title(xCol)
-                        .build())
-                .yAxis(Axis.builder()
-                        .title(yCol)
-                        .build())
-                .build();
+        Layout layout = Layout.builder(title, xCol, yCol).showLegend(true).build();
 
         ScatterTrace[] traces  = new ScatterTrace[tables.size()];
         Marker marker = Marker.builder().opacity(OPACITY).build();
@@ -49,19 +35,7 @@ public class ScatterPlot {
     }
 
     public static Figure create(String title, Table table, String xCol, String yCol) {
-
-        Layout layout = Layout.builder()
-                .title(title)
-                .height(HEIGHT)
-                .width(WIDTH)
-                .xAxis(Axis.builder()
-                        .title(xCol)
-                        .build())
-                .yAxis(Axis.builder()
-                        .title(yCol)
-                        .build())
-                .build();
-
+        Layout layout = Layout.builder(title, xCol, yCol).build();
         ScatterTrace trace = ScatterTrace.builder(
                 table.numberColumn(xCol),
                 table.numberColumn(yCol))
@@ -70,20 +44,9 @@ public class ScatterPlot {
     }
 
     public static Figure create(String title, String xTitle, double[] xCol, String yTitle, double[] yCol) {
-
-        Layout layout = Layout.builder()
-                .title(title)
-                .height(HEIGHT)
-                .width(WIDTH)
-                .xAxis(Axis.builder()
-                        .title(xTitle)
-                        .build())
-                .yAxis(Axis.builder()
-                        .title(yTitle)
-                        .build())
-                .build();
-
+        Layout layout = Layout.builder(title, xTitle, yTitle).build();
         ScatterTrace trace = ScatterTrace.builder(xCol, yCol).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 c38cd53f..fc9fb889 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/api/TimeSeriesPlot.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/api/TimeSeriesPlot.java
@@ -3,7 +3,6 @@ package tech.tablesaw.plotly.api;
 import tech.tablesaw.api.DateColumn;
 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;
@@ -13,14 +12,11 @@ import java.util.List;
 
 public class TimeSeriesPlot {
 
-    private static final int HEIGHT = 600;
-    private static final int WIDTH = 800;
-
     public static Figure create(String title, Table table, String dateColX, String yCol, String groupCol) {
 
         TableSliceGroup tables = table.splitOn(table.categoricalColumn(groupCol));
 
-        Layout layout = standardLayout(title, dateColX, yCol);
+        Layout layout = Layout.builder(title, dateColX, yCol).build();
 
         ScatterTrace[] traces  = new ScatterTrace[tables.size()];
         for (int i = 0; i < tables.size(); i++) {
@@ -38,9 +34,7 @@ public class TimeSeriesPlot {
     }
 
     public static Figure create(String title, Table table, String dateColXName, String yColName) {
-
-        Layout layout = standardLayout(title, dateColXName, yColName);
-
+        Layout layout = Layout.builder(title, dateColXName, yColName).build();
         ScatterTrace trace = ScatterTrace.builder(
                 table.column(dateColXName),
                 table.numberColumn(yColName))
@@ -50,26 +44,11 @@ public class TimeSeriesPlot {
     }
 
     public static Figure create(String title, String xTitle, DateColumn xCol, String yTitle, NumberColumn<?> yCol) {
-
-        Layout layout = standardLayout(title, xTitle, yTitle);
-
+        Layout layout = Layout.builder(title, xTitle, yTitle).build();
         ScatterTrace trace = ScatterTrace.builder(xCol, yCol)
                 .mode(ScatterTrace.Mode.LINE)
                 .build();
         return new Figure(layout, trace);
     }
 
-    private static Layout standardLayout(String title, String xTitle, String yTitle) {
-        return Layout.builder()
-                .title(title)
-                .height(HEIGHT)
-                .width(WIDTH)
-                .xAxis(Axis.builder()
-                        .title(xTitle)
-                        .build())
-                .yAxis(Axis.builder()
-                        .title(yTitle)
-                        .build())
-                .build();
-    }
 }
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 e40fae37..830ddf22 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/components/Layout.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/components/Layout.java
@@ -13,6 +13,8 @@ import java.util.Map;
 
 public class Layout {
 
+    private static final int DEFAULT_HEIGHT = 600;
+    private static final int DEFAULT_WIDTH = 800;
     private final static String DEFAULT_TITLE = "";
     private final static String DEFAULT_PAPER_BG_COLOR = "#fff";
     private final static String DEFAULT_PLOT_BG_COLOR = "#fff";
@@ -287,6 +289,27 @@ public class Layout {
         return new LayoutBuilder();
     }
 
+    public static LayoutBuilder builder(String title) {
+      return Layout.builder()
+          .title(title)
+          .height(DEFAULT_HEIGHT)
+          .width(DEFAULT_WIDTH);
+    }
+
+    public static LayoutBuilder builder(String title, String xTitle) {
+      return Layout.builder(title)
+          .xAxis(Axis.builder()
+                  .title(xTitle)
+                  .build());
+    }
+
+    public static LayoutBuilder builder(String title, String xTitle, String yTitle) {
+      return Layout.builder(title, xTitle)
+          .yAxis(Axis.builder()
+                  .title(yTitle)
+                  .build());
+    }
+
     public static class LayoutBuilder {
 
         /**
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/components/Page.java b/jsplot/src/main/java/tech/tablesaw/plotly/components/Page.java
index e9ff98ed..e4964a6d 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/components/Page.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/components/Page.java
@@ -1,14 +1,14 @@
 package tech.tablesaw.plotly.components;
 
-import com.mitchellbosecke.pebble.error.PebbleException;
-import com.mitchellbosecke.pebble.template.PebbleTemplate;
-
 import java.io.IOException;
 import java.io.StringWriter;
 import java.io.Writer;
 import java.util.HashMap;
 import java.util.Map;
 
+import com.mitchellbosecke.pebble.error.PebbleException;
+import com.mitchellbosecke.pebble.template.PebbleTemplate;
+
 /**
  * Represents an entire html page that contains a figure
  */
@@ -17,9 +17,12 @@ public class Page extends Component {
     private final Figure figure;
     private final String divName;
 
+    private final String plotlyJsLocation;
+
     private Page(PageBuilder builder) {
         this.figure = builder.figure;
         this.divName = builder.divName;
+        this.plotlyJsLocation = builder.plotlyJsLocation;
     }
 
     @Override
@@ -41,6 +44,7 @@ public class Page extends Component {
         context.put("figureScript", figure.asJavascript(divName));
         context.put("targetDiv", figure.divString(divName));
         context.put("figureTitle", figure.getLayout() != null? figure.getLayout().getTitle() : null);
+        context.put("plotlyJsLocation", plotlyJsLocation);
         return context;
     }
 
@@ -53,6 +57,8 @@ public class Page extends Component {
         private final Figure figure;
         private final String divName;
 
+        private String plotlyJsLocation = null;
+
         public PageBuilder(Figure figure, String divName) {
             this.figure = figure;
             this.divName = divName;
@@ -61,5 +67,10 @@ public class Page extends Component {
         public Page build() {
             return new Page(this);
         }
+        
+        public PageBuilder plotlyJsLocation(String location) {
+            this.plotlyJsLocation = location;
+            return this;
+        }
     }
 }
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/components/TemplateUtils.java b/jsplot/src/main/java/tech/tablesaw/plotly/components/TemplateUtils.java
index 31116f1b..4343e4aa 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/components/TemplateUtils.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/components/TemplateUtils.java
@@ -1,16 +1,40 @@
 package tech.tablesaw.plotly.components;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
 import com.mitchellbosecke.pebble.PebbleEngine;
 import com.mitchellbosecke.pebble.error.PebbleException;
 import com.mitchellbosecke.pebble.loader.ClasspathLoader;
+import com.mitchellbosecke.pebble.loader.DelegatingLoader;
+import com.mitchellbosecke.pebble.loader.FileLoader;
 import com.mitchellbosecke.pebble.loader.Loader;
 
 public class TemplateUtils {
 
+    private static Collection<String> templateLocations = new ArrayList<>(); 
+    
+    public static void setTemplateLocations(String... locations) {
+        templateLocations = Arrays.asList(locations);
+    }
+    
     public static PebbleEngine getNewEngine() {
         PebbleEngine engine;
         try {
             Loader<?> loader = new ClasspathLoader();
+            if (templateLocations != null && templateLocations.size() > 0) {
+                List<Loader<?>> loaders = new ArrayList<>();
+                for (String templateLocation : templateLocations) {
+                    FileLoader fileLoader = new FileLoader();
+                    fileLoader.setPrefix(templateLocation);
+                    loaders.add(fileLoader);
+                }
+                // add this one last, so it is shadowed
+                loaders.add(loader);
+                loader = new DelegatingLoader(loaders);
+            }
             engine = new PebbleEngine.Builder().loader(loader).strictVariables(false).build();
         } catch (PebbleException e) {
             e.printStackTrace();
@@ -18,6 +42,4 @@ public class TemplateUtils {
         }
         return engine;
     }
-
-
 }
diff --git a/jsplot/src/main/resources/page_template.html b/jsplot/src/main/resources/page_template.html
index 1f2f080b..5820c547 100644
--- a/jsplot/src/main/resources/page_template.html
+++ b/jsplot/src/main/resources/page_template.html
@@ -3,10 +3,11 @@
 <head>
     <meta charset="UTF-8">
     <title>{{figureTitle}}</title>
-<!--
-    <script src="src/main/resources/plotly-latest.min.js"></script>
--->
+{% if plotlyJsLocation is not null %}
+    <script src="{{plotlyJsLocation}}"></script>
+{% else %}
     <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
+{% endif %}
 </head>
 <body>
 {% if targetDiv is not null %}


Test patch:
diff --git a/core/src/test/java/tech/tablesaw/io/csv/CsvReaderTest.java b/core/src/test/java/tech/tablesaw/io/csv/CsvReaderTest.java
index b2bc9a78..3bb6b915 100644
--- a/core/src/test/java/tech/tablesaw/io/csv/CsvReaderTest.java
+++ b/core/src/test/java/tech/tablesaw/io/csv/CsvReaderTest.java
@@ -508,4 +508,13 @@ public class CsvReaderTest {
         assertEquals("1 rows X 10001 cols", table1.shape());
     }
 
+    @Test
+    public void testSkipLinesWithComments() throws Exception {
+        Table table1 = Table.read().csv(CsvReadOptions
+                .builder("../data/with_comments.csv")
+                .maxNumberOfColumns(3)
+                .commentPrefix('#')
+                .header(true));
+        assertEquals("3 rows X 3 cols", table1.shape());
+    }
 }
diff --git a/jsplot/src/test/java/tech/tablesaw/components/TemplateUtilsTest.java b/jsplot/src/test/java/tech/tablesaw/components/TemplateUtilsTest.java
new file mode 100644
index 00000000..a1e36c47
--- /dev/null
+++ b/jsplot/src/test/java/tech/tablesaw/components/TemplateUtilsTest.java
@@ -0,0 +1,53 @@
+package tech.tablesaw.components;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.net.URL;
+
+import org.junit.jupiter.api.Test;
+
+import tech.tablesaw.plotly.components.Figure;
+import tech.tablesaw.plotly.components.Page;
+import tech.tablesaw.plotly.components.Page.PageBuilder;
+import tech.tablesaw.plotly.components.TemplateUtils;
+import tech.tablesaw.plotly.traces.ScatterTrace;
+import tech.tablesaw.plotly.traces.Trace;
+
+public class TemplateUtilsTest {
+
+    private double[] x = {1, 2, 3, 4, 5};
+    private double[] y = {1, 4, 9, 16, 25};
+
+    private String customTemplateString = "<!-- Custom page_template.html -->";
+    
+    @Test
+    public void testDefaultTemplateLocation() {
+        TemplateUtils.setTemplateLocations();
+        String html = createPageHtml();
+        assertTrue(html.indexOf(customTemplateString) < 0);
+    }
+
+    @Test
+    public void testCustomTemplateLocation() {
+        URL url = this.getClass().getResource(this.getClass().getSimpleName() + ".class");
+        assertNotNull(url, "Couldn't locate class (as resource), where template is also found");
+        String path = url.getPath();
+        assertTrue(path.lastIndexOf('/') >= 0);
+        String folderPath = path.substring(0, path.lastIndexOf('/'));
+        File templateFile = new File(folderPath + "/page_template.html");
+        assertTrue(templateFile.exists(), templateFile + " doesn't exist");
+        TemplateUtils.setTemplateLocations(folderPath);
+        String html = createPageHtml();
+        assertTrue(html.indexOf(customTemplateString) >= 0);
+    }
+
+    private String createPageHtml() {
+        Trace trace = ScatterTrace.builder(x, y).build();
+        Figure figure = new Figure(trace);
+        Page page = new PageBuilder(figure, "plot").build();
+        String html = page.asJavascript();
+        return html;
+    }
+}
diff --git a/jsplot/src/test/java/tech/tablesaw/plotly/PageTest.java b/jsplot/src/test/java/tech/tablesaw/plotly/PageTest.java
new file mode 100644
index 00000000..d0dd3f56
--- /dev/null
+++ b/jsplot/src/test/java/tech/tablesaw/plotly/PageTest.java
@@ -0,0 +1,35 @@
+package tech.tablesaw.plotly;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+import tech.tablesaw.plotly.components.Figure;
+import tech.tablesaw.plotly.components.Page;
+import tech.tablesaw.plotly.traces.BarTrace;
+
+public class PageTest {
+
+    private final Object[] x = {"sheep", "cows", "fish", "tree sloths"};
+    private final double[] y = {1, 4, 9, 16};
+
+    @Test
+    public void testDefaultPlotlyJsLocation() {
+        BarTrace trace = BarTrace.builder(x, y).build();
+        Page page = Page.pageBuilder(new Figure(trace), "plot")
+                .build();
+        String html = page.asJavascript();
+        assertTrue(html.indexOf("\"" + "https://cdn.plot.ly/plotly-latest.min.js" + "\"") > 0);
+    }
+    
+    @Test
+    public void testCustomPlotlyJsLocation() {
+        BarTrace trace = BarTrace.builder(x, y).build();
+        String location = this.getClass().getResource(this.getClass().getSimpleName() + ".class").toString();
+        Page page = Page.pageBuilder(new Figure(trace), "plot")
+                .plotlyJsLocation(location)
+                .build();
+        String html = page.asJavascript();
+        assertTrue(html.indexOf("\"" + location + "\"") > 0);
+    }
+}
diff --git a/jsplot/src/test/resources/tech/tablesaw/components/page_template.html b/jsplot/src/test/resources/tech/tablesaw/components/page_template.html
new file mode 100644
index 00000000..022b5850
--- /dev/null
+++ b/jsplot/src/test/resources/tech/tablesaw/components/page_template.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<!-- Custom page_template.html -->
+<!-- The text above is used by TemplateUtilsTest -->
+<html>
+<head>
+    <meta charset="UTF-8">
+    <title>{{figureTitle}}</title>
+{% if plotlyJsLocation is not null %}
+    <script src="{{plotlyJsLocation}}"></script>
+{% else %}
+    <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
+{% endif %}
+</head>
+<body>
+{% if targetDiv is not null %}
+    {{targetDiv | raw}}
+{% endif %}
+{% if figureScript is not null %}
+    {{figureScript | raw}}
+{% endif %}
+
+</body>
+</html>
\ No newline at end of file