Merge pull request #292 from joanpau
authorDan Vanderkam <danvdk@gmail.com>
Wed, 5 Nov 2014 14:40:38 +0000 (09:40 -0500)
committerDan Vanderkam <danvdk@gmail.com>
Wed, 5 Nov 2014 14:40:38 +0000 (09:40 -0500)
Conflicts:
dygraph-tickers.js

21 files changed:
README
auto_tests/misc/local.js
auto_tests/tests/range_selector.js
bower.json
docs/legal.html
dygraph-canvas.js
dygraph-dev.js
dygraph-internal.externs.js
dygraph-layout.js
dygraph-options-reference.js
dygraph-options.js
dygraph-utils.js
dygraph.js
generate-combined.sh
phantom-driver.js
plugins/legend.js
plugins/range-selector.js
polyfills/console.js [new file with mode: 0644]
push-to-web.sh
stacktrace.js [deleted file]
tests/range-selector.html

diff --git a/README b/README
index 53e19c5..b249e36 100644 (file)
--- a/README
+++ b/README
@@ -3,9 +3,10 @@ Version 1.0.1
 Copyright (c) 2006-, Dan Vanderkam.
 
 Documentation: http://dygraphs.com/
-Support: http://groups.google.com/group/dygraphs-users
+Support: http://stackoverflow.com/questions/tagged/dygraphs
+         http://groups.google.com/group/dygraphs-users
 Source: http://github.com/danvk/dygraphs
-Issues: http://code.google.com/p/dygraphs/
+Issues: https://github.com/danvk/dygraphs/issues
 
 
 The dygraphs JavaScript library produces interactive, zoomable charts of time series.
@@ -52,7 +53,7 @@ dygraphs uses:
  - excanvas.js (Apache License)
  - YUI compressor (BSD License)
  - JsDoc Toolkit (MIT license)
- - stacktrace.js is public domain
+ - console-polyfill (MIT license)
 
 automated tests use:
  - auto_tests/lib/jquery-1.4.2.js (MIT & GPL2)
index 05927a5..4b1ea40 100644 (file)
@@ -24,12 +24,12 @@ var DygraphsLocalTester = function() {
  * In some cases we will still allow warnings to be warnings, however.
  */
 DygraphsLocalTester.prototype.overrideWarn = function() {
-  // save Dygraph.warn so we can catch warnings.
-  var originalDygraphWarn = Dygraph.warn;
-  Dygraph.warn = function(msg) {
-    // This warning is still
+  // save console.warn so we can catch warnings.
+  var originalWarn = console.warn;
+  console.warn = function(msg) {
+    // This warning is pervasive enough that we'll let it slide (for now).
     if (msg == "Using default labels. Set labels explicitly via 'labels' in the options parameter") {
-      originalDygraphWarn(msg);
+      originalWarn(msg);
       return;
     }
     throw 'Warnings not permitted: ' + msg;
index 9f83188..aed2b3e 100644 (file)
@@ -451,6 +451,39 @@ RangeSelectorTestCase.prototype.testCombinedSeries = function() {
   }, combinedSeries);
 };
 
+// Tests selection of a specific series to average for the mini plot.
+RangeSelectorTestCase.prototype.testSelectedCombinedSeries = function() {
+  var opts = {
+    showRangeSelector: true,
+    labels: ['X', 'Y1', 'Y2', 'Y3', 'Y4'],
+    series: {
+      'Y1': { showInRangeSelector: true },
+      'Y3': { showInRangeSelector: true }
+    }
+  };
+  var data = [
+      [0, 5, 8, 13, 21],  // average (first and third) = 9
+      [5, 1, 3, 7, 14],   // average (first and third) = 4
+      [10, 0, 19, 10, 6]  // average (first and third) = 5
+    ];
+  var graph = document.getElementById("graph");
+  var g = new Dygraph(graph, data, opts);
+
+  var rangeSelector = g.getPluginInstance_(Dygraph.Plugins.RangeSelector);
+  assertNotNull(rangeSelector);
+
+  var combinedSeries = rangeSelector.computeCombinedSeriesAndLimits_();
+  assertEquals({
+    yMin: 4 - 5 * 0.25,  // 25% padding on combined series range.
+    yMax: 9 + 5 * 0.25,
+    data: [
+      [0, 9],
+      [5, 4],
+      [10, 5]
+    ]
+  }, combinedSeries);
+};
+
 // Tests data computation for the mini plot with a single error bar series.
 RangeSelectorTestCase.prototype.testSingleCombinedSeriesCustomBars = function() {
   var opts = {
index 4aa054d..9cb1240 100644 (file)
@@ -36,7 +36,6 @@
     "release.sh",
     "releases.json",
     "screenshot.png",
-    "stacktrace.js",
     "test.sh",
     "tests",
     "thumbnail.png",
index 3e3d176..6b99843 100644 (file)
@@ -10,7 +10,7 @@
  - excanvas.js (Apache License)
  - YUI compressor (BSD License)
  - JsDoc Toolkit (MIT license)
- - stacktrace.js is public domain
+ - console-polyfill (MIT license)
 
 automated tests use:
  - auto_tests/lib/jquery-1.4.2.js (MIT &amp; GPL2)
index 17908e3..efcad10 100644 (file)
@@ -559,7 +559,7 @@ DygraphCanvasRenderer._errorPlotter = function(e) {
 
   var fillGraph = g.getBooleanOption("fillGraph", setName);
   if (fillGraph) {
-    Dygraph.warn("Can't use fillGraph option with error bars");
+    console.warn("Can't use fillGraph option with error bars");
   }
 
   var ctx = e.drawingContext;
index 6adfa51..26a96df 100644 (file)
@@ -16,7 +16,7 @@
   // This list needs to be kept in sync w/ the one in generate-combined.sh
   // and the one in jsTestDriver.conf.
   var source_files = [
-    "stacktrace.js",
+    "polyfills/console.js",
     "dashed-canvas.js",
     "dygraph-options.js",
     "dygraph-layout.js",
index 92e5cc3..dabfa10 100644 (file)
@@ -2,12 +2,6 @@
 // - declares symbols that are provided outisde of dygraphs (e.g. by excanvas)
 // - defines custom types used internally
 
-/**
- * @param {Object} dict
- * @return {!Array.<string>}
- */
-function printStackTrace(dict) {}
-
 
 /**
  * @constructor
index befffd6..bf3d7e0 100644 (file)
@@ -149,13 +149,13 @@ DygraphLayout.prototype.setAnnotations = function(ann) {
   for (var i = 0; i < ann.length; i++) {
     var a = {};
     if (!ann[i].xval && ann[i].x === undefined) {
-      Dygraph.error("Annotations must have an 'x' property");
+      console.error("Annotations must have an 'x' property");
       return;
     }
     if (ann[i].icon &&
         !(ann[i].hasOwnProperty('width') &&
           ann[i].hasOwnProperty('height'))) {
-      Dygraph.error("Must set width and height when setting " +
+      console.error("Must set width and height when setting " +
                     "annotation.icon property");
       return;
     }
@@ -204,7 +204,7 @@ DygraphLayout.prototype._evaluateLimits = function() {
       axis.ylogrange = Dygraph.log10(axis.maxyval) - Dygraph.log10(axis.minyval);
       axis.ylogscale = (axis.ylogrange !== 0 ? 1.0 / axis.ylogrange : 1.0);
       if (!isFinite(axis.ylogrange) || isNaN(axis.ylogrange)) {
-        Dygraph.error('axis ' + i + ' of graph at ' + axis.g +
+        console.error('axis ' + i + ' of graph at ' + axis.g +
                       ' can\'t be displayed in log scale for range [' +
                       axis.minyval + ' - ' + axis.maxyval + ']');
       }
index 60bd81e..8520c10 100644 (file)
@@ -799,6 +799,12 @@ Dygraph.OPTIONS_REFERENCE =  // <JSON>
     "type": "string",
     "description": "The range selector mini plot fill color. This can be of the form \"#AABBCC\" or \"rgb(255,100,200)\" or \"yellow\". You can also specify null or \"\" to turn off fill."
   },
+  "showInRangeSelector": {
+    "default": "null",
+    "labels": ["Interactive Elements"],
+    "type": "boolean",
+    "description": "Mark this series for inclusion in the range selector. The mini plot curve will be an average of all such series. If this is not specified for any series, the default behavior is to average all the series. Setting it for one series will result in that series being charted alone in the range selector."
+  },
   "animatedZooms": {
     "default": "false",
     "labels": ["Interactive Elements"],
index 98ef35f..b0fad61 100644 (file)
@@ -183,7 +183,7 @@ DygraphOptions.prototype.reparseSeries = function() {
 
       if (typeof(axis) == 'string') {
         if (!this.series_.hasOwnProperty(axis)) {
-          Dygraph.error("Series " + seriesName + " wants to share a y-axis with " +
+          console.error("Series " + seriesName + " wants to share a y-axis with " +
                      "series " + axis + ", which does not define its own axis.");
           return;
         }
index 68a3148..73777b9 100644 (file)
@@ -12,7 +12,7 @@
  */
 
 /*jshint globalstrict: true */
-/*global Dygraph:false, G_vmlCanvasManager:false, Node:false, printStackTrace: false */
+/*global Dygraph:false, G_vmlCanvasManager:false, Node:false */
 "use strict";
 
 Dygraph.LOG_SCALE = 10;
@@ -27,20 +27,6 @@ Dygraph.log10 = function(x) {
   return Math.log(x) / Dygraph.LN_TEN;
 };
 
-// Various logging levels.
-Dygraph.DEBUG = 1;
-Dygraph.INFO = 2;
-Dygraph.WARNING = 3;
-Dygraph.ERROR = 3;
-
-// <REMOVE_FOR_COMBINED>
-// Set this to log stack traces on warnings, etc.
-// This requires stacktrace.js, which is up to you to provide.
-// A copy can be found in the dygraphs repo, or at
-// https://github.com/eriwen/javascript-stacktrace
-Dygraph.LOG_STACK_TRACES = false;
-// </REMOVE_FOR_COMBINED>
-
 /** A dotted line stroke pattern. */
 Dygraph.DOTTED_LINE = [2, 2];
 /** A dashed line stroke pattern. */
@@ -49,94 +35,6 @@ Dygraph.DASHED_LINE = [7, 3];
 Dygraph.DOT_DASH_LINE = [7, 2, 2, 2];
 
 /**
- * Log an error on the JS console at the given severity.
- * @param {number} severity One of Dygraph.{DEBUG,INFO,WARNING,ERROR}
- * @param {string} message The message to log.
- * @private
- */
-Dygraph.log = function(severity, message) {
-  // <REMOVE_FOR_COMBINED>
-  var st;
-  if (typeof(printStackTrace) != 'undefined') {
-    try {
-      // Remove uninteresting bits: logging functions and paths.
-      st = printStackTrace({guess:false});
-      while (st[0].indexOf("stacktrace") != -1) {
-        st.splice(0, 1);
-      }
-
-      st.splice(0, 2);
-      for (var i = 0; i < st.length; i++) {
-        st[i] = st[i].replace(/\([^)]*\/(.*)\)/, '@$1')
-            .replace(/\@.*\/([^\/]*)/, '@$1')
-            .replace('[object Object].', '');
-      }
-      var top_msg = st.splice(0, 1)[0];
-      message += ' (' + top_msg.replace(/^.*@ ?/, '') + ')';
-    } catch(e) {
-      // Oh well, it was worth a shot!
-    }
-  }
-  // </REMOVE_FOR_COMBINED>
-
-  if (typeof(window.console) != 'undefined') {
-    // In older versions of Firefox, only console.log is defined.
-    var console = window.console;
-    var log = function(console, method, msg) {
-      if (method && typeof(method) == 'function') {
-        method.call(console, msg);
-      } else {
-        console.log(msg);
-      }
-    };
-
-    switch (severity) {
-      case Dygraph.DEBUG:
-        log(console, console.debug, 'dygraphs: ' + message);
-        break;
-      case Dygraph.INFO:
-        log(console, console.info, 'dygraphs: ' + message);
-        break;
-      case Dygraph.WARNING:
-        log(console, console.warn, 'dygraphs: ' + message);
-        break;
-      case Dygraph.ERROR:
-        log(console, console.error, 'dygraphs: ' + message);
-        break;
-    }
-  }
-
-  // <REMOVE_FOR_COMBINED>
-  if (Dygraph.LOG_STACK_TRACES) {
-    window.console.log(st.join('\n'));
-  }
-  // </REMOVE_FOR_COMBINED>
-};
-
-/**
- * @param {string} message
- * @private
- */
-Dygraph.info = function(message) {
-  Dygraph.log(Dygraph.INFO, message);
-};
-
-/**
- * @param {string} message
- * @private
- */
-Dygraph.warn = function(message) {
-  Dygraph.log(Dygraph.WARNING, message);
-};
-
-/**
- * @param {string} message
- */
-Dygraph.error = function(message) {
-  Dygraph.log(Dygraph.ERROR, message);
-};
-
-/**
  * Return the 2d context for a dygraph canvas.
  *
  * This method is only exposed for the sake of replacing the function in
@@ -672,7 +570,7 @@ Dygraph.dateParser = function(dateStr) {
   }
 
   if (!d || isNaN(d)) {
-    Dygraph.error("Couldn't parse " + dateStr + " as a date");
+    console.error("Couldn't parse " + dateStr + " as a date");
   }
   return d;
 };
@@ -1305,7 +1203,7 @@ Dygraph.parseFloat_ = function(x, opt_line_no, opt_line) {
   if (opt_line !== undefined && opt_line_no !== undefined) {
     msg += " on line " + (1+(opt_line_no||0)) + " ('" + opt_line + "') of CSV.";
   }
-  Dygraph.error(msg);
+  console.error(msg);
 
   return null;
 };
index bba1028..7b80e32 100644 (file)
@@ -72,7 +72,7 @@ var Dygraph = function(div, data, opts, opt_fourth_param) {
     // Old versions of dygraphs took in the series labels as a constructor
     // parameter. This doesn't make sense anymore, but it's easy to continue
     // to support this usage.
-    Dygraph.warn("Using deprecated four-argument dygraph constructor");
+    console.warn("Using deprecated four-argument dygraph constructor");
     this.__old_init__(div, data, opts, opt_fourth_param);
   } else {
     this.__init__(div, data, opts);
@@ -151,7 +151,7 @@ Dygraph.numberValueFormatter = function(x, opts, pt, g) {
       k_labels = Dygraph.KMB_LABELS;
     }
     if (kmg2) {
-      if (kmb) Dygraph.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
+      if (kmb) console.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
       k = 1024;
       k_labels = Dygraph.KMG2_BIG_LABELS;
       m_labels = Dygraph.KMG2_SMALL_LABELS;
@@ -345,6 +345,7 @@ Dygraph.DEFAULT_ATTRS = {
   rangeSelectorHeight: 40,
   rangeSelectorPlotStrokeColor: "#808FAB",
   rangeSelectorPlotFillColor: "#A7B1C4",
+  showInRangeSelector: null,
 
   // The ordering here ensures that central lines always appear above any
   // fill bars/error bars.
@@ -444,7 +445,7 @@ Dygraph.prototype.__init__ = function(div, file, attrs) {
   }
 
   if (!div) {
-    Dygraph.error("Constructing dygraph with a non-existent div!");
+    console.error("Constructing dygraph with a non-existent div!");
     return;
   }
 
@@ -666,9 +667,9 @@ Dygraph.prototype.toString = function() {
 Dygraph.prototype.attr_ = function(name, seriesName) {
 // <REMOVE_FOR_COMBINED>
   if (typeof(Dygraph.OPTIONS_REFERENCE) === 'undefined') {
-    Dygraph.error('Must include options reference JS for testing');
+    console.error('Must include options reference JS for testing');
   } else if (!Dygraph.OPTIONS_REFERENCE.hasOwnProperty(name)) {
-    Dygraph.error('Dygraphs is using property ' + name + ', which has no ' +
+    console.error('Dygraphs is using property ' + name + ', which has no ' +
                   'entry in the Dygraphs.OPTIONS_REFERENCE listing.');
     // Only log this error once.
     Dygraph.OPTIONS_REFERENCE[name] = true;
@@ -2673,7 +2674,7 @@ Dygraph.prototype.drawGraph_ = function() {
 
   if (this.getStringOption("timingName")) {
     var end = new Date();
-    Dygraph.info(this.getStringOption("timingName") + " - drawGraph: " + (end - start) + "ms");
+    console.log(this.getStringOption("timingName") + " - drawGraph: " + (end - start) + "ms");
   }
 };
 
@@ -3095,7 +3096,7 @@ Dygraph.prototype.parseCSV_ = function(data) {
         // TODO(danvk): figure out an appropriate way to flag parse errors.
         vals = inFields[j].split("/");
         if (vals.length != 2) {
-          Dygraph.error('Expected fractional "num/den" values in CSV data ' +
+          console.error('Expected fractional "num/den" values in CSV data ' +
                         "but found a value '" + inFields[j] + "' on line " +
                         (1 + i) + " ('" + line + "') which is not of this form.");
           fields[j] = [0, 0];
@@ -3107,7 +3108,7 @@ Dygraph.prototype.parseCSV_ = function(data) {
     } else if (this.getBooleanOption("errorBars")) {
       // If there are error bars, values are (value, stddev) pairs
       if (inFields.length % 2 != 1) {
-        Dygraph.error('Expected alternating (value, stdev.) pairs in CSV data ' +
+        console.error('Expected alternating (value, stdev.) pairs in CSV data ' +
                       'but line ' + (1 + i) + ' has an odd number of values (' +
                       (inFields.length - 1) + "): '" + line + "'");
       }
@@ -3128,7 +3129,7 @@ Dygraph.prototype.parseCSV_ = function(data) {
                           Dygraph.parseFloat_(vals[1], i, line),
                           Dygraph.parseFloat_(vals[2], i, line) ];
           } else {
-            Dygraph.warn('When using customBars, values must be either blank ' +
+            console.warn('When using customBars, values must be either blank ' +
                          'or "low;center;high" tuples (got "' + val +
                          '" on line ' + (1+i));
           }
@@ -3145,7 +3146,7 @@ Dygraph.prototype.parseCSV_ = function(data) {
     }
 
     if (fields.length != expectedCols) {
-      Dygraph.error("Number of columns in line " + i + " (" + fields.length +
+      console.error("Number of columns in line " + i + " (" + fields.length +
                     ") does not agree with number of labels (" + expectedCols +
                     ") " + line);
     }
@@ -3160,7 +3161,7 @@ Dygraph.prototype.parseCSV_ = function(data) {
         if (fields[j]) all_null = false;
       }
       if (all_null) {
-        Dygraph.warn("The dygraphs 'labels' option is set, but the first row " +
+        console.warn("The dygraphs 'labels' option is set, but the first row " +
                      "of CSV data ('" + line + "') appears to also contain " +
                      "labels. Will drop the CSV labels and use the option " +
                      "labels.");
@@ -3171,7 +3172,7 @@ Dygraph.prototype.parseCSV_ = function(data) {
   }
 
   if (outOfOrder) {
-    Dygraph.warn("CSV is out of order; order it correctly to speed loading.");
+    console.warn("CSV is out of order; order it correctly to speed loading.");
     ret.sort(function(a,b) { return a[0] - b[0]; });
   }
 
@@ -3189,17 +3190,17 @@ Dygraph.prototype.parseCSV_ = function(data) {
 Dygraph.prototype.parseArray_ = function(data) {
   // Peek at the first x value to see if it's numeric.
   if (data.length === 0) {
-    Dygraph.error("Can't plot empty data set");
+    console.error("Can't plot empty data set");
     return null;
   }
   if (data[0].length === 0) {
-    Dygraph.error("Data set cannot contain an empty row");
+    console.error("Data set cannot contain an empty row");
     return null;
   }
 
   var i;
   if (this.attr_("labels") === null) {
-    Dygraph.warn("Using default labels. Set labels explicitly via 'labels' " +
+    console.warn("Using default labels. Set labels explicitly via 'labels' " +
                  "in the options parameter");
     this.attrs_.labels = [ "X" ];
     for (i = 1; i < data[0].length; i++) {
@@ -3209,7 +3210,7 @@ Dygraph.prototype.parseArray_ = function(data) {
   } else {
     var num_labels = this.attr_("labels");
     if (num_labels.length != data[0].length) {
-      Dygraph.error("Mismatch between number of labels (" + num_labels + ")" +
+      console.error("Mismatch between number of labels (" + num_labels + ")" +
                     " and number of columns in array (" + data[0].length + ")");
       return null;
     }
@@ -3225,13 +3226,13 @@ Dygraph.prototype.parseArray_ = function(data) {
     var parsedData = Dygraph.clone(data);
     for (i = 0; i < data.length; i++) {
       if (parsedData[i].length === 0) {
-        Dygraph.error("Row " + (1 + i) + " of data is empty");
+        console.error("Row " + (1 + i) + " of data is empty");
         return null;
       }
       if (parsedData[i][0] === null ||
           typeof(parsedData[i][0].getTime) != 'function' ||
           isNaN(parsedData[i][0].getTime())) {
-        Dygraph.error("x value in row " + (1 + i) + " is not a Date");
+        console.error("x value in row " + (1 + i) + " is not a Date");
         return null;
       }
       parsedData[i][0] = parsedData[i][0].getTime();
@@ -3285,7 +3286,7 @@ Dygraph.prototype.parseDataTable_ = function(data) {
     this.attrs_.axes.x.ticker = Dygraph.numericTicks;
     this.attrs_.axes.x.axisLabelFormatter = this.attrs_.axes.x.valueFormatter;
   } else {
-    Dygraph.error("only 'date', 'datetime' and 'number' types are supported " +
+    console.error("only 'date', 'datetime' and 'number' types are supported " +
                   "for column 1 of DataTable input (Got '" + indepType + "')");
     return null;
   }
@@ -3309,7 +3310,7 @@ Dygraph.prototype.parseDataTable_ = function(data) {
       }
       hasAnnotations = true;
     } else {
-      Dygraph.error("Only 'number' is supported as a dependent type with Gviz." +
+      console.error("Only 'number' is supported as a dependent type with Gviz." +
                     " 'string' is only supported if displayAnnotations is true");
     }
   }
@@ -3331,7 +3332,7 @@ Dygraph.prototype.parseDataTable_ = function(data) {
     var row = [];
     if (typeof(data.getValue(i, 0)) === 'undefined' ||
         data.getValue(i, 0) === null) {
-      Dygraph.warn("Ignoring row " + i +
+      console.warn("Ignoring row " + i +
                    " of DataTable because of undefined or null first column.");
       continue;
     }
@@ -3377,7 +3378,7 @@ Dygraph.prototype.parseDataTable_ = function(data) {
   }
 
   if (outOfOrder) {
-    Dygraph.warn("DataTable is out of order; order it correctly to speed loading.");
+    console.warn("DataTable is out of order; order it correctly to speed loading.");
     ret.sort(function(a,b) { return a[0] - b[0]; });
   }
   this.rawData_ = ret;
@@ -3439,7 +3440,7 @@ Dygraph.prototype.start_ = function() {
       req.send(null);
     }
   } else {
-    Dygraph.error("Unknown data format: " + (typeof data));
+    console.error("Unknown data format: " + (typeof data));
   }
 };
 
@@ -3530,7 +3531,7 @@ Dygraph.mapLegacyOptions_ = function(attrs) {
   };
   var map = function(opt, axis, new_opt) {
     if (typeof(attrs[opt]) != 'undefined') {
-      Dygraph.warn("Option " + opt + " is deprecated. Use the " +
+      console.warn("Option " + opt + " is deprecated. Use the " +
           new_opt + " option for the " + axis + " axis instead. " +
           "(e.g. { axes : { " + axis + " : { " + new_opt + " : ... } } } " +
           "(see http://dygraphs.com/per-axis.html for more information.");
@@ -3573,7 +3574,7 @@ Dygraph.prototype.resize = function(width, height) {
   this.resize_lock = true;
 
   if ((width === null) != (height === null)) {
-    Dygraph.warn("Dygraph.resize() should be called with zero parameters or " +
+    console.warn("Dygraph.resize() should be called with zero parameters or " +
                  "two non-NULL parameters. Pretending it was zero.");
     width = height = null;
   }
@@ -3636,7 +3637,7 @@ Dygraph.prototype.visibility = function() {
 Dygraph.prototype.setVisibility = function(num, value) {
   var x = this.visibility();
   if (num < 0 || num >= x.length) {
-    Dygraph.warn("invalid series number in setVisibility: " + num);
+    console.warn("invalid series number in setVisibility: " + num);
   } else {
     x[num] = value;
     this.predraw_();
@@ -3664,7 +3665,7 @@ Dygraph.prototype.setAnnotations = function(ann, suppressDraw) {
   Dygraph.addAnnotationRule();
   this.annotations_ = ann;
   if (!this.layout_) {
-    Dygraph.warn("Tried to setAnnotations before dygraph was ready. " +
+    console.warn("Tried to setAnnotations before dygraph was ready. " +
                  "Try setting them in a ready() block. See " +
                  "dygraphs.com/tests/annotation.html");
     return;
@@ -3761,5 +3762,5 @@ Dygraph.addAnnotationRule = function() {
     }
   }
 
-  Dygraph.warn("Unable to add default annotation CSS rule; display may be off.");
+  console.warn("Unable to add default annotation CSS rule; display may be off.");
 };
index 7cdc22a..30c0a7e 100755 (executable)
@@ -5,6 +5,7 @@ GetSources () {
   # This list needs to be kept in sync w/ the one in dygraph-dev.js
   # and the one in jsTestDriver.conf. Order matters, except for the plugins.
   for F in \
+    polyfills/console.js \
     dashed-canvas.js \
     dygraph-options.js \
     dygraph-layout.js \
index 22f4687..c534964 100644 (file)
@@ -63,9 +63,6 @@ page.open(url, function(status) {
   results = page.evaluate(function() {
     var num_passing = 0, num_failing = 0;
     var failures = [];
-    // Phantom doesn't like stacktrace.js using the "arguments" object
-    // in stacktrace.js, which it interprets in strict mode.
-    printStackTrace = undefined;
 
     jstestdriver.attachListener({
       finish : function(tc, name, result, e) {
index fb93b41..d0a6fc8 100644 (file)
@@ -88,7 +88,7 @@ legend.prototype.activate = function(g) {
       try {
         div.style[name] = messagestyle[name];
       } catch (e) {
-        Dygraph.warn("You are using unsupported css properties for your " +
+        console.warn("You are using unsupported css properties for your " +
             "browser in labelsDivStyles");
       }
     }
index fe32b1a..717f850 100644 (file)
@@ -52,8 +52,8 @@ rangeSelector.prototype.destroy = function() {
 // Private methods
 //------------------------------------------------------------------
 
-rangeSelector.prototype.getOption_ = function(name) {
-  return this.dygraph_.getOption(name);
+rangeSelector.prototype.getOption_ = function(name, opt_series) {
+  return this.dygraph_.getOption(name, opt_series);
 };
 
 rangeSelector.prototype.setDefaultOption_ = function(name, value) {
@@ -74,7 +74,7 @@ rangeSelector.prototype.createInterface_ = function() {
 
   // Range selector and animatedZooms have a bad interaction. See issue 359.
   if (this.getOption_('animatedZooms')) {
-    Dygraph.warn('Animated zooms and range selector are not compatible; disabling animatedZooms.');
+    console.warn('Animated zooms and range selector are not compatible; disabling animatedZooms.');
     this.dygraph_.updateOptions({animatedZooms: false}, true);
   }
 
@@ -544,7 +544,7 @@ rangeSelector.prototype.drawStaticLayer_ = function() {
   try {
     this.drawMiniPlot_();
   } catch(ex) {
-    Dygraph.warn(ex);
+    console.warn(ex);
   }
 
   var margin = 0.5;
@@ -594,6 +594,13 @@ rangeSelector.prototype.drawMiniPlot_ = function() {
     var dataPoint = combinedSeriesData.data[i];
     var x = ((dataPoint[0] !== null) ? ((dataPoint[0] - xExtremes[0])*xFact) : NaN);
     var y = ((dataPoint[1] !== null) ? (canvasHeight - (dataPoint[1] - combinedSeriesData.yMin)*yFact) : NaN);
+
+    // Skip points that don't change the x-value. Overly fine-grained points
+    // can cause major slowdowns with the ctx.fill() call below.
+    if (!stepPlot && prevX !== null && Math.round(x) == Math.round(prevX)) {
+      continue;
+    }
+
     if (isFinite(x) && isFinite(y)) {
       if(prevX === null) {
         ctx.lineTo(x, canvasHeight);
@@ -646,15 +653,29 @@ rangeSelector.prototype.drawMiniPlot_ = function() {
 rangeSelector.prototype.computeCombinedSeriesAndLimits_ = function() {
   var g = this.dygraph_;
   var logscale = this.getOption_('logscale');
-
-  // Create a combined series (average of all series values).
   var i;
 
+  // Select series to combine. By default, all series are combined.
+  var numColumns = g.numColumns();
+  var labels = g.getLabels();
+  var includeSeries = new Array(numColumns);
+  var anySet = false;
+  for (i = 1; i < numColumns; i++) {
+    var include = this.getOption_('showInRangeSelector', labels[i]);
+    includeSeries[i] = include;
+    if (include !== null) anySet = true;  // it's set explicitly for this series
+  }
+  if (!anySet) {
+    for (i = 0; i < includeSeries.length; i++) includeSeries[i] = true;
+  }
+
+  // Create a combined series (average of selected series values).
   // TODO(danvk): short-circuit if there's only one series.
   var rolledSeries = [];
   var dataHandler = g.dataHandler_;
   var options = g.attributes_;
   for (i = 1; i < g.numColumns(); i++) {
+    if (!includeSeries[i]) continue;
     var series = dataHandler.extractSeries(g.rawData_, i, options);
     if (g.rollPeriod() > 1) {
       series = dataHandler.rollingAverage(series, g.rollPeriod(), options);
diff --git a/polyfills/console.js b/polyfills/console.js
new file mode 100644 (file)
index 0000000..4a6f542
--- /dev/null
@@ -0,0 +1,15 @@
+// Console-polyfill. MIT license.
+// https://github.com/paulmillr/console-polyfill
+// Make it safe to do console.log() always.
+(function(con) {
+  'use strict';
+  var prop, method;
+  var empty = {};
+  var dummy = function() {};
+  var properties = 'memory'.split(',');
+  var methods = ('assert,clear,count,debug,dir,dirxml,error,exception,group,' +
+     'groupCollapsed,groupEnd,info,log,markTimeline,profile,profiles,profileEnd,' +
+     'show,table,time,timeEnd,timeline,timelineEnd,timeStamp,trace,warn').split(',');
+  while (prop = properties.pop()) con[prop] = con[prop] || empty;
+  while (method = methods.pop()) con[method] = con[method] || dummy;
+})(this.console = this.console || {}); // Using `this` for web workers.
index 232790e..785c4a1 100755 (executable)
@@ -30,9 +30,9 @@ if [ -s docs/options.html ] ; then
   find . -path ./.git -prune -o -print | xargs chmod a+rX
 
   # Copy everything to the site.
-  rsync -avzr gallery common tests jsdoc experimental plugins datahandler $site \
+  rsync -avzr gallery common tests jsdoc experimental plugins datahandler polyfills $site \
   && \
-  rsync -avzr --copy-links dashed-canvas.js stacktrace.js dygraph*.js gadget.xml excanvas.js thumbnail.png screenshot.png $temp_dir/* $site/
+  rsync -avzr --copy-links dashed-canvas.js dygraph*.js gadget.xml excanvas.js thumbnail.png screenshot.png $temp_dir/* $site/
 else
   echo "generate-documentation.py failed"
 fi
diff --git a/stacktrace.js b/stacktrace.js
deleted file mode 100644 (file)
index 4d0384d..0000000
+++ /dev/null
@@ -1,411 +0,0 @@
-// Domain Public by Eric Wendelin http://eriwen.com/ (2008)
-//                  Luke Smith http://lucassmith.name/ (2008)
-//                  Loic Dachary <loic@dachary.org> (2008)
-//                  Johan Euphrosine <proppy@aminche.com> (2008)
-//                  Oyvind Sean Kinsey http://kinsey.no/blog (2010)
-//                  Victor Homyakov <victor-homyakov@users.sourceforge.net> (2010)
-
-/**
- * Main function giving a function stack trace with a forced or passed in Error
- *
- * @cfg {Error} e The error to create a stacktrace from (optional)
- * @cfg {Boolean} guess If we should try to resolve the names of anonymous functions
- * @return {Array} of Strings with functions, lines, files, and arguments where possible
- */
-function printStackTrace(options) {
-    options = options || {guess: true};
-    var ex = options.e || null, guess = !!options.guess;
-    var p = new printStackTrace.implementation(), result = p.run(ex);
-    return (guess) ? p.guessAnonymousFunctions(result) : result;
-}
-
-printStackTrace.implementation = function() {
-};
-
-printStackTrace.implementation.prototype = {
-    /**
-     * @param {Error} ex The error to create a stacktrace from (optional)
-     * @param {String} mode Forced mode (optional, mostly for unit tests)
-     */
-    run: function(ex, mode) {
-        ex = ex || this.createException();
-        // examine exception properties w/o debugger
-        //for (var prop in ex) {alert("Ex['" + prop + "']=" + ex[prop]);}
-        mode = mode || this.mode(ex);
-        if (mode === 'other') {
-            return this.other(arguments.callee);
-        } else {
-            return this[mode](ex);
-        }
-    },
-
-    createException: function() {
-        try {
-            this.undef();
-        } catch (e) {
-            return e;
-        }
-    },
-
-    /**
-     * Mode could differ for different exception, e.g.
-     * exceptions in Chrome may or may not have arguments or stack.
-     *
-     * @return {String} mode of operation for the exception
-     */
-    mode: function(e) {
-        if (e['arguments'] && e.stack) {
-            return 'chrome';
-        } else if (typeof e.message === 'string' && typeof window !== 'undefined' && window.opera) {
-            // e.message.indexOf("Backtrace:") > -1 -> opera
-            // !e.stacktrace -> opera
-            if (!e.stacktrace) {
-                return 'opera9'; // use e.message
-            }
-            // 'opera#sourceloc' in e -> opera9, opera10a
-            if (e.message.indexOf('\n') > -1 && e.message.split('\n').length > e.stacktrace.split('\n').length) {
-                return 'opera9'; // use e.message
-            }
-            // e.stacktrace && !e.stack -> opera10a
-            if (!e.stack) {
-                return 'opera10a'; // use e.stacktrace
-            }
-            // e.stacktrace && e.stack -> opera10b
-            if (e.stacktrace.indexOf("called from line") < 0) {
-                return 'opera10b'; // use e.stacktrace, format differs from 'opera10a'
-            }
-            // e.stacktrace && e.stack -> opera11
-            return 'opera11'; // use e.stacktrace, format differs from 'opera10a', 'opera10b'
-        } else if (e.stack) {
-            return 'firefox';
-        }
-        return 'other';
-    },
-
-    /**
-     * Given a context, function name, and callback function, overwrite it so that it calls
-     * printStackTrace() first with a callback and then runs the rest of the body.
-     *
-     * @param {Object} context of execution (e.g. window)
-     * @param {String} functionName to instrument
-     * @param {Function} function to call with a stack trace on invocation
-     */
-    instrumentFunction: function(context, functionName, callback) {
-        context = context || window;
-        var original = context[functionName];
-        context[functionName] = function instrumented() {
-            callback.call(this, printStackTrace().slice(4));
-            return context[functionName]._instrumented.apply(this, arguments);
-        };
-        context[functionName]._instrumented = original;
-    },
-
-    /**
-     * Given a context and function name of a function that has been
-     * instrumented, revert the function to it's original (non-instrumented)
-     * state.
-     *
-     * @param {Object} context of execution (e.g. window)
-     * @param {String} functionName to de-instrument
-     */
-    deinstrumentFunction: function(context, functionName) {
-        if (context[functionName].constructor === Function &&
-                context[functionName]._instrumented &&
-                context[functionName]._instrumented.constructor === Function) {
-            context[functionName] = context[functionName]._instrumented;
-        }
-    },
-
-    /**
-     * Given an Error object, return a formatted Array based on Chrome's stack string.
-     *
-     * @param e - Error object to inspect
-     * @return Array<String> of function calls, files and line numbers
-     */
-    chrome: function(e) {
-        var stack = (e.stack + '\n').replace(/^\S[^\(]+?[\n$]/gm, '').
-          replace(/^\s+at\s+/gm, '').
-          replace(/^([^\(]+?)([\n$])/gm, '{anonymous}()@$1$2').
-          replace(/^Object.<anonymous>\s*\(([^\)]+)\)/gm, '{anonymous}()@$1').split('\n');
-        stack.pop();
-        return stack;
-    },
-
-    /**
-     * Given an Error object, return a formatted Array based on Firefox's stack string.
-     *
-     * @param e - Error object to inspect
-     * @return Array<String> of function calls, files and line numbers
-     */
-    firefox: function(e) {
-        return e.stack.replace(/(?:\n@:0)?\s+$/m, '').replace(/^\(/gm, '{anonymous}(').split('\n');
-    },
-
-    opera11: function(e) {
-        // "Error thrown at line 42, column 12 in <anonymous function>() in file://localhost/G:/js/stacktrace.js:\n"
-        // "Error thrown at line 42, column 12 in <anonymous function: createException>() in file://localhost/G:/js/stacktrace.js:\n"
-        // "called from line 7, column 4 in bar(n) in file://localhost/G:/js/test/functional/testcase1.html:\n"
-        // "called from line 15, column 3 in file://localhost/G:/js/test/functional/testcase1.html:\n"
-        var ANON = '{anonymous}', lineRE = /^.*line (\d+), column (\d+)(?: in (.+))? in (\S+):$/;
-        var lines = e.stacktrace.split('\n'), result = [];
-
-        for (var i = 0, len = lines.length; i < len; i += 2) {
-            var match = lineRE.exec(lines[i]);
-            if (match) {
-                var location = match[4] + ':' + match[1] + ':' + match[2];
-                var fnName = match[3] || "global code";
-                fnName = fnName.replace(/<anonymous function: (\S+)>/, "$1").replace(/<anonymous function>/, ANON);
-                result.push(fnName + '@' + location + ' -- ' + lines[i + 1].replace(/^\s+/, ''));
-            }
-        }
-
-        return result;
-    },
-
-    opera10b: function(e) {
-        // "<anonymous function: run>([arguments not available])@file://localhost/G:/js/stacktrace.js:27\n" +
-        // "printStackTrace([arguments not available])@file://localhost/G:/js/stacktrace.js:18\n" +
-        // "@file://localhost/G:/js/test/functional/testcase1.html:15"
-        var ANON = '{anonymous}', lineRE = /^(.*)@(.+):(\d+)$/;
-        var lines = e.stacktrace.split('\n'), result = [];
-
-        for (var i = 0, len = lines.length; i < len; i++) {
-            var match = lineRE.exec(lines[i]);
-            if (match) {
-                var fnName = match[1]? (match[1] + '()') : "global code";
-                result.push(fnName + '@' + match[2] + ':' + match[3]);
-            }
-        }
-
-        return result;
-    },
-
-    /**
-     * Given an Error object, return a formatted Array based on Opera 10's stacktrace string.
-     *
-     * @param e - Error object to inspect
-     * @return Array<String> of function calls, files and line numbers
-     */
-    opera10a: function(e) {
-        // "  Line 27 of linked script file://localhost/G:/js/stacktrace.js\n"
-        // "  Line 11 of inline#1 script in file://localhost/G:/js/test/functional/testcase1.html: In function foo\n"
-        var ANON = '{anonymous}', lineRE = /Line (\d+).*script (?:in )?(\S+)(?:: In function (\S+))?$/i;
-        var lines = e.stacktrace.split('\n'), result = [];
-
-        for (var i = 0, len = lines.length; i < len; i += 2) {
-            var match = lineRE.exec(lines[i]);
-            if (match) {
-                var fnName = match[3] || ANON;
-                result.push(fnName + '()@' + match[2] + ':' + match[1] + ' -- ' + lines[i + 1].replace(/^\s+/, ''));
-            }
-        }
-
-        return result;
-    },
-
-    // Opera 7.x-9.2x only!
-    opera9: function(e) {
-        // "  Line 43 of linked script file://localhost/G:/js/stacktrace.js\n"
-        // "  Line 7 of inline#1 script in file://localhost/G:/js/test/functional/testcase1.html\n"
-        var ANON = '{anonymous}', lineRE = /Line (\d+).*script (?:in )?(\S+)/i;
-        var lines = e.message.split('\n'), result = [];
-
-        for (var i = 2, len = lines.length; i < len; i += 2) {
-            var match = lineRE.exec(lines[i]);
-            if (match) {
-                result.push(ANON + '()@' + match[2] + ':' + match[1] + ' -- ' + lines[i + 1].replace(/^\s+/, ''));
-            }
-        }
-
-        return result;
-    },
-
-    // Safari, IE, and others
-    other: function(curr) {
-        var ANON = '{anonymous}', fnRE = /function\s*([\w\-$]+)?\s*\(/i, stack = [], fn, args, maxStackSize = 10;
-        while (curr && stack.length < maxStackSize) {
-            fn = fnRE.test(curr.toString()) ? RegExp.$1 || ANON : ANON;
-            args = Array.prototype.slice.call(curr['arguments'] || []);
-            stack[stack.length] = fn + '(' + this.stringifyArguments(args) + ')';
-            curr = curr.caller;
-        }
-        return stack;
-    },
-
-    /**
-     * Given arguments array as a String, subsituting type names for non-string types.
-     *
-     * @param {Arguments} object
-     * @return {Array} of Strings with stringified arguments
-     */
-    stringifyArguments: function(args) {
-        var result = [];
-        var slice = Array.prototype.slice;
-        for (var i = 0; i < args.length; ++i) {
-            var arg = args[i];
-            if (arg === undefined) {
-                result[i] = 'undefined';
-            } else if (arg === null) {
-                result[i] = 'null';
-            } else if (arg.constructor) {
-                if (arg.constructor === Array) {
-                    if (arg.length < 3) {
-                        result[i] = '[' + this.stringifyArguments(arg) + ']';
-                    } else {
-                        result[i] = '[' + this.stringifyArguments(slice.call(arg, 0, 1)) + '...' + this.stringifyArguments(slice.call(arg, -1)) + ']';
-                    }
-                } else if (arg.constructor === Object) {
-                    result[i] = '#object';
-                } else if (arg.constructor === Function) {
-                    result[i] = '#function';
-                } else if (arg.constructor === String) {
-                    result[i] = '"' + arg + '"';
-                } else if (arg.constructor === Number) {
-                    result[i] = arg;
-                }
-            }
-        }
-        return result.join(',');
-    },
-
-    sourceCache: {},
-
-    /**
-     * @return the text from a given URL
-     */
-    ajax: function(url) {
-        var req = this.createXMLHTTPObject();
-        if (req) {
-            try {
-                req.open('GET', url, false);
-                req.send(null);
-                return req.responseText;
-            } catch (e) {
-            }
-        }
-        return '';
-    },
-
-    /**
-     * Try XHR methods in order and store XHR factory.
-     *
-     * @return <Function> XHR function or equivalent
-     */
-    createXMLHTTPObject: function() {
-        var xmlhttp, XMLHttpFactories = [
-            function() {
-                return new XMLHttpRequest();
-            }, function() {
-                return new ActiveXObject('Msxml2.XMLHTTP');
-            }, function() {
-                return new ActiveXObject('Msxml3.XMLHTTP');
-            }, function() {
-                return new ActiveXObject('Microsoft.XMLHTTP');
-            }
-        ];
-        for (var i = 0; i < XMLHttpFactories.length; i++) {
-            try {
-                xmlhttp = XMLHttpFactories[i]();
-                // Use memoization to cache the factory
-                this.createXMLHTTPObject = XMLHttpFactories[i];
-                return xmlhttp;
-            } catch (e) {
-            }
-        }
-    },
-
-    /**
-     * Given a URL, check if it is in the same domain (so we can get the source
-     * via Ajax).
-     *
-     * @param url <String> source url
-     * @return False if we need a cross-domain request
-     */
-    isSameDomain: function(url) {
-        return url.indexOf(location.hostname) !== -1;
-    },
-
-    /**
-     * Get source code from given URL if in the same domain.
-     *
-     * @param url <String> JS source URL
-     * @return <Array> Array of source code lines
-     */
-    getSource: function(url) {
-        // TODO reuse source from script tags?
-        if (!(url in this.sourceCache)) {
-            this.sourceCache[url] = this.ajax(url).split('\n');
-        }
-        return this.sourceCache[url];
-    },
-
-    guessAnonymousFunctions: function(stack) {
-        for (var i = 0; i < stack.length; ++i) {
-            var reStack = /\{anonymous\}\(.*\)@(.*)/,
-                reRef = /^(.*?)(?::(\d+))(?::(\d+))?(?: -- .+)?$/,
-                frame = stack[i], ref = reStack.exec(frame);
-
-            if (ref) {
-                var m = reRef.exec(ref[1]), file = m[1],
-                    lineno = m[2], charno = m[3] || 0;
-                if (file && this.isSameDomain(file) && lineno) {
-                    var functionName = this.guessAnonymousFunction(file, lineno, charno);
-                    stack[i] = frame.replace('{anonymous}', functionName);
-                }
-            }
-        }
-        return stack;
-    },
-
-    guessAnonymousFunction: function(url, lineNo, charNo) {
-        var ret;
-        try {
-            ret = this.findFunctionName(this.getSource(url), lineNo);
-        } catch (e) {
-            ret = 'getSource failed with url: ' + url + ', exception: ' + e.toString();
-        }
-        return ret;
-    },
-
-    findFunctionName: function(source, lineNo) {
-        // FIXME findFunctionName fails for compressed source
-        // (more than one function on the same line)
-        // TODO use captured args
-        // function {name}({args}) m[1]=name m[2]=args
-        var reFunctionDeclaration = /function\s+([^(]*?)\s*\(([^)]*)\)/;
-        // {name} = function ({args}) TODO args capture
-        // /['"]?([0-9A-Za-z_]+)['"]?\s*[:=]\s*function(?:[^(]*)/
-        var reFunctionExpression = /['"]?([0-9A-Za-z_]+)['"]?\s*[:=]\s*function\b/;
-        // {name} = eval()
-        var reFunctionEvaluation = /['"]?([0-9A-Za-z_]+)['"]?\s*[:=]\s*(?:eval|new Function)\b/;
-        // Walk backwards in the source lines until we find
-        // the line which matches one of the patterns above
-        var code = "", line, maxLines = Math.min(lineNo, 20), m, commentPos;
-        for (var i = 0; i < maxLines; ++i) {
-            // lineNo is 1-based, source[] is 0-based
-            line = source[lineNo - i - 1];
-            commentPos = line.indexOf('//');
-            if (commentPos >= 0) {
-                line = line.substr(0, commentPos);
-            }
-            // TODO check other types of comments? Commented code may lead to false positive
-            if (line) {
-                code = line + code;
-                m = reFunctionExpression.exec(code);
-                if (m && m[1]) {
-                    return m[1];
-                }
-                m = reFunctionDeclaration.exec(code);
-                if (m && m[1]) {
-                    //return m[1] + "(" + (m[2] || "") + ")";
-                    return m[1];
-                }
-                m = reFunctionEvaluation.exec(code);
-                if (m && m[1]) {
-                    return m[1];
-                }
-            }
-        }
-        return '(?)';
-    }
-};
index 2cb9615..8527b5c 100644 (file)
     </p>
     <div id="roll14" style="width:800px; height:320px;"></div>
     <p>
+      Use the average of a specific subset of series to draw the mini plot (only the first series is used in this test).
+      The default behaviour is to compute the average of <em>all</em> series.
+    </p>
+    <div id="selectcombined" style="width:800px; height:320px;"></div>
+    <p>
       Demo of range selecor without the chart. (interesting if multiple charts should be synced with one range selector).
     </p>
     <div id="nochart" style="width:800px; height:30px;"></div>
+    <p>Demo of range selector with stepPlot</p>
+    <div id="stepplot" style="width:800px; height:320px;"></div>
+
     <script type="text/javascript">
       g1 = new Dygraph(
           document.getElementById("noroll"),
           }
       );
       g3 = new Dygraph(
+          document.getElementById("selectcombined"),
+          [
+            [0, 1, 4, 10],
+            [10, 2, 8, 19],
+            [25, 15, 4, 2],
+            [35, 0, 3, 2]
+          ],
+          {
+            title: 'Daily Temperatures in New York vs. San Francisco',
+            ylabel: 'Temperature (F)',
+            showRangeSelector: true,
+            labels: ['X', 'Y1', 'Y2', 'Y3'],
+            series: {
+              'Y1': { showInRangeSelector: true }
+            }
+          }
+      );
+      g4 = new Dygraph(
           document.getElementById("nochart"),
           [[0,1],[10,1]],
           {
             rangeSelectorHeight: 30
           }
       );
+      g5 = new Dygraph(document.getElementById("stepplot"),
+                      "Date,Idle,Used\n" +
+                      "2008-05-07,70,30\n" +
+                      "2008-05-08,42,88\n" +
+                      "2008-05-09,88,42\n" +
+                      "2008-05-10,33,37\n" +
+                      "2008-05-11,30,35\n",
+                       {
+                          stepPlot: true,
+                          fillGraph: true,
+                          stackedGraph: true,
+                          includeZero: true,
+                          showRangeSelector: true
+                       });
     </script>
   </body>
 </html>