Repository: jtablesaw/tablesaw
Pull request #700 (2019-10-29 19:55:57)

Enhancements to Histogram
Support for probability histograms (See issue #681) via histnorm property
Support for bin summary calculations in histograms via histfunc property
Support for rendering a histogram horizontally

Examples for each.
Existing tests work

Thanks for contributing.

- [x] Tick to sign-off your agreement to the [Developer Certificate of Origin (DCO) 1.1](https://developercertificate.org)

## Description

What was changed
see above

## Testing
Existing tests and new example code to visually test new functionality



Patch:
diff --git a/core/src/main/java/tech/tablesaw/columns/Column.java b/core/src/main/java/tech/tablesaw/columns/Column.java
index 7f8b459e..4808eade 100644
--- a/core/src/main/java/tech/tablesaw/columns/Column.java
+++ b/core/src/main/java/tech/tablesaw/columns/Column.java
@@ -327,11 +327,13 @@ public interface Column<T> extends Iterable<T>, Comparator<T> {
   }
 
   /**
-   * Maps the function across all rows, appending the results to the provided Column
+   * Maps the function across all rows, storing the results into the provided Column.
+   *
+   * <p>The target column must have at least the same number of rows.
    *
    * @param fun function to map
-   * @param into Column to which results are appended
-   * @return the provided Column, to which results are appended
+   * @param into Column into which results are set
+   * @return the provided Column
    */
   default <R, C extends Column<R>> C mapInto(Function<? super T, ? extends R> fun, C into) {
     for (int i = 0; i < size(); i++) {
@@ -344,6 +346,34 @@ public interface Column<T> extends Iterable<T>, Comparator<T> {
     return into;
   }
 
+  /**
+   * Maps the function across all rows, appending the results to the created Column.
+   *
+   * <p>Example:
+   *
+   * <pre>
+   * DoubleColumn d;
+   * StringColumn s = d.map(String::valueOf, StringColumn::create);
+   * </pre>
+   *
+   * @param fun function to map
+   * @param creator the creator of the Column. Its String argument will be the name of the current
+   *     column (see {@link #name()})
+   * @return the Column with the results
+   */
+  default <R, C extends Column<R>> C map(
+      Function<? super T, ? extends R> fun, Function<String, C> creator) {
+    C into = creator.apply(name());
+    for (int i = 0; i < size(); i++) {
+      if (isMissing(i)) {
+        into.appendMissing();
+      } else {
+        into.append(fun.apply(get(i)));
+      }
+    }
+    return into;
+  }
+
   Column<T> setMissing(int i);
 
   /**
diff --git a/core/src/main/java/tech/tablesaw/io/FileReader.java b/core/src/main/java/tech/tablesaw/io/FileReader.java
index 82366e18..ab7a04ff 100644
--- a/core/src/main/java/tech/tablesaw/io/FileReader.java
+++ b/core/src/main/java/tech/tablesaw/io/FileReader.java
@@ -21,7 +21,11 @@ public abstract class FileReader {
   /** Constructs a CsvReader */
   public FileReader() {}
 
-  protected ColumnType[] getTypes(
+  /**
+   * Returns an array containing the inferred columnTypes for the file being read, as calculated by
+   * the ColumnType inference logic. These types may not be correct.
+   */
+  public ColumnType[] getColumnTypes(
       Reader reader, ReadOptions options, int linesToSkip, AbstractParser<?> parser) {
 
     parser.beginParsing(reader);
@@ -56,7 +60,8 @@ public abstract class FileReader {
     return name.trim();
   }
 
-  private String[] getHeaderNames(
+  /** Returns the column names for each column in the source. */
+  public String[] getColumnNames(
       ReadOptions options, ColumnType[] types, AbstractParser<?> parser) {
     if (options.header()) {
       String[] headerNames = parser.parseNext();
@@ -88,7 +93,7 @@ public abstract class FileReader {
     parser.beginParsing(reader);
     Table table = Table.create(options.tableName());
 
-    List<String> headerRow = Lists.newArrayList(getHeaderNames(options, types, parser));
+    List<String> headerRow = Lists.newArrayList(getColumnNames(options, types, parser));
 
     for (int x = 0; x < types.length; x++) {
       if (types[x] != SKIP) {
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 1ec21f81..29209618 100644
--- a/core/src/main/java/tech/tablesaw/io/csv/CsvReader.java
+++ b/core/src/main/java/tech/tablesaw/io/csv/CsvReader.java
@@ -137,7 +137,7 @@ public class CsvReader extends FileReader implements DataReader<CsvReadOptions>
     CsvParser parser = csvParser(options);
 
     try {
-      return getTypes(reader, options, linesToSkip, parser);
+      return getColumnTypes(reader, options, linesToSkip, parser);
     } finally {
       parser.stopParsing();
       // we don't close the reader since we didn't create it
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 28dee235..6ff6a490 100644
--- a/core/src/main/java/tech/tablesaw/io/fixed/FixedWidthReader.java
+++ b/core/src/main/java/tech/tablesaw/io/fixed/FixedWidthReader.java
@@ -138,7 +138,7 @@ public class FixedWidthReader extends FileReader implements DataReader<FixedWidt
     AbstractParser<?> parser = fixedWidthParser(options);
 
     try {
-      return getTypes(reader, options, linesToSkip, parser);
+      return getColumnTypes(reader, options, linesToSkip, parser);
     } finally {
       parser.stopParsing();
       // we don't close the reader since we didn't create it
diff --git a/html/src/main/java/tech/tablesaw/io/html/HtmlReader.java b/html/src/main/java/tech/tablesaw/io/html/HtmlReader.java
index be80cab3..de351e7f 100644
--- a/html/src/main/java/tech/tablesaw/io/html/HtmlReader.java
+++ b/html/src/main/java/tech/tablesaw/io/html/HtmlReader.java
@@ -52,7 +52,9 @@ public class HtmlReader implements DataReader<HtmlReadOptions> {
               "Table index outside bounds. The URL has " + tables.size() + " tables");
         }
       } else {
-          throw new IllegalStateException(tables.size() + " tables found. When more than one html table is present on the page you must specify the index of the table to read from.");
+        throw new IllegalStateException(
+            tables.size()
+                + " tables found. When more than one html table is present on the page you must specify the index of the table to read from.");
       }
     }
     Element htmlTable = tables.get(tableIndex);
diff --git a/jsplot/pom.xml b/jsplot/pom.xml
index aaa6312d..a6ff1a3f 100644
--- a/jsplot/pom.xml
+++ b/jsplot/pom.xml
@@ -90,6 +90,10 @@
     </build>
 
     <dependencies>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-core</artifactId>
+        </dependency>
         <dependency>
             <groupId>io.pebbletemplates</groupId>
             <artifactId>pebble</artifactId>
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/Utils.java b/jsplot/src/main/java/tech/tablesaw/plotly/Utils.java
index 3267a0e6..09326b4a 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/Utils.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/Utils.java
@@ -1,5 +1,6 @@
 package tech.tablesaw.plotly;
 
+import com.fasterxml.jackson.core.io.JsonStringEncoder;
 import java.util.Arrays;
 
 public class Utils {
@@ -8,17 +9,27 @@ public class Utils {
     return Arrays.toString(data);
   }
 
+  /** @return un-escaped quote of argument */
   public static String quote(String string) {
     return "'" + string + "'";
   }
 
+  /**
+   * Escapes string for Javascript, assuming but without surrounding it with doublequotes (") and
+   * saves to output to the given StringBuilder.
+   */
+  private static void escape(String s, StringBuilder sb) {
+    JsonStringEncoder.getInstance().quoteAsString(s, sb);
+  }
+
+  /** @return a Javascript array of strings (escaped if needed) */
   public static String dataAsString(Object[] data) {
     StringBuilder builder = new StringBuilder("[");
     for (int i = 0; i < data.length; i++) {
       Object o = data[i];
-      builder.append("'");
-      builder.append(o);
-      builder.append("'");
+      builder.append("\"");
+      escape(String.valueOf(o), builder);
+      builder.append("\"");
       if (i < data.length - 1) {
         builder.append(",");
       }
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 b5ff894d..6759a2a8 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/traces/HistogramTrace.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/traces/HistogramTrace.java
@@ -7,18 +7,60 @@ import java.io.StringWriter;
 import java.io.Writer;
 import java.util.Map;
 import tech.tablesaw.api.NumericColumn;
+import tech.tablesaw.columns.Column;
 import tech.tablesaw.plotly.Utils;
 import tech.tablesaw.plotly.components.Marker;
 
 public class HistogramTrace extends AbstractTrace {
 
-  private final double[] x;
+  private final Object[] x;
+  private final Object[] y;
   private final double opacity;
   private final int nBinsX;
   private final int nBinsY;
   private final boolean autoBinX;
   private final boolean autoBinY;
   private final Marker marker;
+  private final HistNorm histNorm;
+  private final HistFunc histFunc;
+
+  public enum HistNorm {
+    NONE(""),
+    PERCENT("percent"),
+    PROBABILITY("probability"),
+    DENSITY("density"),
+    PROBABILITY_DENSITY("probability density");
+
+    private final String value;
+
+    HistNorm(String value) {
+      this.value = value;
+    }
+
+    @Override
+    public String toString() {
+      return value;
+    }
+  }
+
+  public enum HistFunc {
+    COUNT("count"),
+    SUM("sum"),
+    AVG("avg"),
+    MIN("min"),
+    MAX("max");
+
+    private final String value;
+
+    HistFunc(String value) {
+      this.value = value;
+    }
+
+    @Override
+    public String toString() {
+      return value;
+    }
+  }
 
   public static HistogramBuilder builder(double[] values) {
     return new HistogramBuilder(values);
@@ -28,9 +70,21 @@ public class HistogramTrace extends AbstractTrace {
     return new HistogramBuilder(values.asDoubleArray());
   }
 
+  public static HistogramBuilder builder(Column<?> xValues, NumericColumn<? extends Number> values) {
+    return new HistogramBuilder(xValues.asObjectArray(), values.asDoubleArray());
+  }
+
   private HistogramTrace(HistogramBuilder builder) {
     super(builder);
-    this.x = builder.x;
+    if (builder.horizontal) {
+      this.x = builder.y;
+      this.y = builder.x;
+    } else {
+      this.x = builder.x;
+      this.y = builder.y;
+    }
+    this.histNorm = builder.histNorm;
+    this.histFunc = builder.histFunc;
     this.nBinsX = builder.nBinsX;
     this.nBinsY = builder.nBinsY;
     this.autoBinX = builder.autoBinX;
@@ -57,12 +111,19 @@ public class HistogramTrace extends AbstractTrace {
 
     Map<String, Object> context = super.getContext();
     context.put("variableName", "trace" + i);
-    context.put("x", Utils.dataAsString(x));
+    if (x != null) {
+      context.put("x", Utils.dataAsString(x));
+    }
+    if (y != null) {
+      context.put("y", Utils.dataAsString(y));
+    }
     context.put("opacity", opacity);
     context.put("nBinsX", nBinsX);
     context.put("nBinsY", nBinsY);
     context.put("autoBinX", autoBinX);
     context.put("autoBinY", autoBinY);
+    context.put("histnorm", histNorm);
+    context.put("histfunc", histFunc);
     if (marker != null) {
       context.put("marker", marker);
     }
@@ -77,11 +138,20 @@ public class HistogramTrace extends AbstractTrace {
     private int nBinsY;
     private boolean autoBinX;
     private boolean autoBinY;
-    private final double[] x;
+    private boolean horizontal = false;
+    private Object[] x;
+    private Object[] y;
     private Marker marker;
+    private HistNorm histNorm = HistNorm.NONE;
+    private HistFunc histFunc = HistFunc.COUNT;
 
     private HistogramBuilder(double[] values) {
-      this.x = values;
+      this.x = doublesToObjects(values);
+    }
+
+    private HistogramBuilder(Object[] xValues, double[] yValues) {
+      this.x = xValues;
+      this.y = doublesToObjects(yValues);
     }
 
     /**
@@ -126,11 +196,49 @@ public class HistogramTrace extends AbstractTrace {
       return this;
     }
 
+    public HistogramBuilder horizontal(boolean horizontal) {
+      this.horizontal = horizontal;
+      return this;
+    }
+
     public HistogramBuilder showLegend(boolean b) {
       super.showLegend(b);
       return this;
     }
 
+    /**
+     * Specifies the type of normalization used for this histogram trace. If "", the span of each
+     * bar corresponds to the number of occurrences (i.e. the number of data points lying inside the
+     * bins). If "percent" / "probability", the span of each bar corresponds to the percentage /
+     * fraction of occurrences with respect to the total number of sample points (here, the sum of
+     * all bin HEIGHTS equals 100% / 1). If "density", the span of each bar corresponds to the
+     * number of occurrences in a bin divided by the size of the bin interval (here, the sum of all
+     * bin AREAS equals the total number of sample points). If "probability density", the area of
+     * each bar corresponds to the probability that an event will fall into the corresponding bin
+     * (here, the sum of all bin AREAS equals 1).
+     *
+     * @param histNorm  The normalization type for the histogram
+     * @return          This HistogramBuilder
+     */
+    public HistogramBuilder histNorm(HistNorm histNorm) {
+      this.histNorm = histNorm;
+      return this;
+    }
+
+    /**
+     * Specifies the binning function used for this histogram trace. If "count", the histogram
+     * values are computed by counting the number of values lying inside each bin. If "sum", "avg",
+     * "min", "max", the histogram values are computed using the sum, the average, the minimum or
+     * the maximum of the values lying inside each bin respectively.
+     *
+     * @param histFunc  The function type
+     * @return          This HistogramBuilder
+     */
+    public HistogramBuilder histFunc(HistFunc histFunc) {
+      this.histFunc = histFunc;
+      return this;
+    }
+
     public HistogramBuilder name(String name) {
       super.name(name);
       return this;
@@ -146,6 +254,16 @@ public class HistogramTrace extends AbstractTrace {
       return this;
     }
 
+    public HistogramBuilder y(double[] y) {
+      this.y = doublesToObjects(y);
+      return this;
+    }
+
+    public HistogramBuilder y(NumericColumn<? extends Number> values) {
+      this.y = values.asObjectArray();
+      return this;
+    }
+
     public HistogramTrace build() {
       return new HistogramTrace(this);
     }
@@ -155,4 +273,12 @@ public class HistogramTrace extends AbstractTrace {
       return type;
     }
   }
+
+  private static Object[] doublesToObjects(double[] doubles) {
+    Object[] objects = new Object[doubles.length];
+    for (int i = 0; i < doubles.length; i++) {
+      objects[i] = doubles[i];
+    }
+    return objects;
+  }
 }
diff --git a/jsplot/src/main/resources/trace_template.html b/jsplot/src/main/resources/trace_template.html
index 096c5624..703c7f72 100644
--- a/jsplot/src/main/resources/trace_template.html
+++ b/jsplot/src/main/resources/trace_template.html
@@ -1,6 +1,8 @@
 var {{variableName}} =
 {
+{% if x is not null %}
 x: {{x | raw}},
+{% endif %}
 {% if y is not null %}
 y: {{y | raw}},
 {% endif %}
@@ -61,6 +63,12 @@ nbinsy: {{nBinsY}},
 {% if autoBinY is not null %}
 autobiny: {{autoBinY}},
 {% endif %}
+{% if histnorm is not null %}
+    histnorm: '{{histnorm}}',
+{% endif %}
+{% if histfunc is not null %}
+    histfunc: '{{histfunc}}',
+{% endif %}
 {% if visible is not null %}
 visible: '{{visible}}',
 {% endif %}
diff --git a/pom.xml b/pom.xml
index d94bc8c3..72bc1942 100644
--- a/pom.xml
+++ b/pom.xml
@@ -252,10 +252,15 @@
 
     <dependencyManagement>
         <dependencies>
+            <dependency>
+                <groupId>com.fasterxml.jackson.core</groupId>
+                <artifactId>jackson-core</artifactId>
+                <version>2.10.1</version>
+            </dependency>
             <dependency>
                 <groupId>com.fasterxml.jackson.core</groupId>
                 <artifactId>jackson-databind</artifactId>
-                <version>2.9.10</version>
+                <version>2.10.1</version>
             </dependency>
             <dependency>
                 <groupId>org.slf4j</groupId>


Test patch:
diff --git a/core/src/test/java/tech/tablesaw/columns/ColumnTest.java b/core/src/test/java/tech/tablesaw/columns/ColumnTest.java
index 0ce3f478..f89efd04 100644
--- a/core/src/test/java/tech/tablesaw/columns/ColumnTest.java
+++ b/core/src/test/java/tech/tablesaw/columns/ColumnTest.java
@@ -278,6 +278,14 @@ public class ColumnTest {
         DoubleColumn.create("t1", new double[] {-1, 0, 1}).map(negate), 1.0, -0.0, -1.0);
   }
 
+  @Test
+  public void testMap2() {
+    StringColumn c =
+        DoubleColumn.create("t1", new double[] {-1, 0, 1})
+            .map(String::valueOf, StringColumn::create);
+    assertContentEquals(c, "-1.0", "0.0", "1.0");
+  }
+
   @Test
   public void testMaxComparator() {
     assertEquals(
diff --git a/jsplot/src/test/java/tech/tablesaw/examples/HistogramFuncExample.java b/jsplot/src/test/java/tech/tablesaw/examples/HistogramFuncExample.java
new file mode 100644
index 00000000..75cda566
--- /dev/null
+++ b/jsplot/src/test/java/tech/tablesaw/examples/HistogramFuncExample.java
@@ -0,0 +1,52 @@
+/*
+ * 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.IntColumn;
+import tech.tablesaw.api.StringColumn;
+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.HistogramTrace;
+
+import static tech.tablesaw.plotly.traces.HistogramTrace.HistFunc.*;
+
+/** */
+public class HistogramFuncExample {
+
+  public static void main(String[] args) {
+
+    Table test = Table.create(
+            StringColumn.create("type").append("apples").append("apples").append("apples").append("oranges").append("bananas"),
+            IntColumn.create("num").append(5).append(10).append(3).append(10).append(5));
+
+    Layout layout1 = Layout.builder().title("Histogram COUNT Test (team batting averages)").build();
+    HistogramTrace trace = HistogramTrace.
+            builder(test.stringColumn("type"), test.intColumn("num"))
+            .histFunc(COUNT)
+            .build();
+
+    Plot.show(new Figure(layout1, trace));
+
+    Layout layout2 = Layout.builder().title("Hist SUM Test (team batting averages)").build();
+    HistogramTrace trace2 = HistogramTrace.
+            builder(test.stringColumn("type"), test.intColumn("num"))
+            .histFunc(SUM)
+            .build();
+
+    Plot.show(new Figure(layout2, trace2));
+  }
+}
diff --git a/jsplot/src/test/java/tech/tablesaw/examples/HistogramHorizontalExample.java b/jsplot/src/test/java/tech/tablesaw/examples/HistogramHorizontalExample.java
new file mode 100644
index 00000000..5afa75bc
--- /dev/null
+++ b/jsplot/src/test/java/tech/tablesaw/examples/HistogramHorizontalExample.java
@@ -0,0 +1,63 @@
+/*
+ * 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.Layout;
+import tech.tablesaw.plotly.components.Marker;
+import tech.tablesaw.plotly.traces.HistogramTrace;
+import tech.tablesaw.table.TableSliceGroup;
+
+/** */
+public class HistogramHorizontalExample {
+
+  public static void main(String[] args) throws Exception {
+    Table baseball = Table.read().csv("../data/baseball.csv");
+
+    Layout layout =
+        Layout.builder()
+            .title("Distribution of team batting averages")
+            .barMode(Layout.BarMode.OVERLAY)
+            .showLegend(true)
+            .build();
+
+    TableSliceGroup groups = baseball.splitOn("league");
+
+    Table t1 = groups.get(0).asTable();
+    Table t2 = groups.get(1).asTable();
+
+    HistogramTrace trace1 =
+        HistogramTrace.builder(t1.nCol("BA"))
+            .name("American League")
+            .opacity(.75)
+            .nBinsY(24)
+            .horizontal(true)
+            .marker(Marker.builder().color("#FF4136").build())
+            .build();
+
+    HistogramTrace trace2 =
+        HistogramTrace.builder(t2.nCol("BA"))
+            .name("National League")
+            .opacity(.75)
+            .nBinsY(24)
+            .horizontal(true)
+            .marker(Marker.builder().color("#7FDBFF").build())
+            .build();
+
+    Plot.show(new Figure(layout, trace1, trace2));
+  }
+}
diff --git a/jsplot/src/test/java/tech/tablesaw/examples/HistogramProbabilityExample.java b/jsplot/src/test/java/tech/tablesaw/examples/HistogramProbabilityExample.java
new file mode 100644
index 00000000..8c9774fd
--- /dev/null
+++ b/jsplot/src/test/java/tech/tablesaw/examples/HistogramProbabilityExample.java
@@ -0,0 +1,37 @@
+/*
+ * 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.Layout;
+import tech.tablesaw.plotly.traces.HistogramTrace;
+
+/** */
+public class HistogramProbabilityExample {
+
+  public static void main(String[] args) throws Exception {
+    Table baseball = Table.read().csv("../data/baseball.csv");
+
+    Layout layout = Layout.builder().title("Probability Histogram of team batting averages").build();
+    HistogramTrace trace = HistogramTrace.
+            builder(baseball.nCol("BA"))
+            .histNorm(HistogramTrace.HistNorm.PROBABILITY)
+            .build();
+
+    Plot.show(new Figure(layout, trace));
+  }
+}
diff --git a/jsplot/src/test/java/tech/tablesaw/plotly/UtilsTest.java b/jsplot/src/test/java/tech/tablesaw/plotly/UtilsTest.java
new file mode 100644
index 00000000..2e1df5dc
--- /dev/null
+++ b/jsplot/src/test/java/tech/tablesaw/plotly/UtilsTest.java
@@ -0,0 +1,17 @@
+package tech.tablesaw.plotly;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+public class UtilsTest {
+
+  @Test
+  public void testEscapeQuote() {
+    String s = Utils.dataAsString(new String[] {"Bobby\"s tables"});
+    assertTrue(s.contains("\\"), s);
+    String s2 = Utils.dataAsString(new String[] {"Hello\\"});
+    assertEquals("[\"Hello\\\\\"]", s2);
+  }
+}
diff --git a/jsplot/src/test/java/tech/tablesaw/plotly/components/FigureTest.java b/jsplot/src/test/java/tech/tablesaw/plotly/components/FigureTest.java
index 61998a18..ac9fa1a8 100644
--- a/jsplot/src/test/java/tech/tablesaw/plotly/components/FigureTest.java
+++ b/jsplot/src/test/java/tech/tablesaw/plotly/components/FigureTest.java
@@ -25,8 +25,8 @@ class FigureTest {
             + "        \n"
             + "var trace0 =\n"
             + "{\n"
-            + "x: ['1.0','2.0','3.0','4.0','5.0'],\n"
-            + "y: ['1.0','4.0','9.0','16.0','25.0'],\n"
+            + "x: [\"1.0\",\"2.0\",\"3.0\",\"4.0\",\"5.0\"],\n"
+            + "y: [\"1.0\",\"4.0\",\"9.0\",\"16.0\",\"25.0\"],\n"
             + "mode: 'markers',\n"
             + "xaxis: 'x',\n"
             + "yaxis: 'y',\n"
@@ -91,8 +91,8 @@ class FigureTest {
             + "\n"
             + "var trace0 =\n"
             + "{\n"
-            + "x: ['1.0','2.0','3.0','4.0','5.0'],\n"
-            + "y: ['1.0','4.0','9.0','16.0','25.0'],\n"
+            + "x: [\"1.0\",\"2.0\",\"3.0\",\"4.0\",\"5.0\"],\n"
+            + "y: [\"1.0\",\"4.0\",\"9.0\",\"16.0\",\"25.0\"],\n"
             + "mode: 'markers',\n"
             + "xaxis: 'x',\n"
             + "yaxis: 'y',\n"