text: var d3 = require('d3');
  var Scatter = require('@src/traces/scatter');
  var makeBubbleSizeFn = require('@src/traces/scatter/make_bubble_size_func');
  var linePoints = require('@src/traces/scatter/line_points');
  var Lib = require('@src/lib');
  
  var Plotly = require('@lib/index');
  var createGraphDiv = require('../assets/create_graph_div');
  var destroyGraphDiv = require('../assets/destroy_graph_div');
  var customAssertions = require('../assets/custom_assertions');
  var fail = require('../assets/fail_test');
  
  var assertClip = customAssertions.assertClip;
  var assertNodeDisplay = customAssertions.assertNodeDisplay;
  
  var getOpacity = function(node) { return Number(node.style.opacity); };
  var getFillOpacity = function(node) { return Number(node.style['fill-opacity']); };
  var getColor = function(node) { return node.style.fill; };
  var getMarkerSize = function(node) {
      // find path arc multiply by 2 to get the corresponding marker.size value
      // (works for circles only)
      return d3.select(node).attr('d').split('A')[1].split(',')[0] * 2;
  };
  
  describe('Test scatter', function() {
      'use strict';
  
      describe('supplyDefaults', function() {
          var traceIn,
              traceOut;
  
          var defaultColor = '#444',
              layout = {};
  
          var supplyDefaults = Scatter.supplyDefaults;
  
          beforeEach(function() {
              traceOut = {};
          });
  
          it('should set visible to false when x and y are empty', function() {
              traceIn = {};
              supplyDefaults(traceIn, traceOut, defaultColor, layout);
              expect(traceOut.visible).toBe(false);
  
              traceIn = {
                  x: [],
                  y: []
              };
              supplyDefaults(traceIn, traceOut, defaultColor, layout);
              expect(traceOut.visible).toBe(false);
          });
  
          it('should set visible to false when x or y is empty', function() {
              traceIn = {
                  x: []
              };
              supplyDefaults(traceIn, traceOut, defaultColor, layout);
              expect(traceOut.visible).toBe(false);
  
              traceIn = {
                  x: [],
                  y: [1, 2, 3]
              };
              supplyDefaults(traceIn, traceOut, defaultColor, layout);
              expect(traceOut.visible).toBe(false);
  
              traceIn = {
                  y: []
              };
              supplyDefaults(traceIn, traceOut, defaultColor, layout);
              expect(traceOut.visible).toBe(false);
  
              traceIn = {
                  x: [1, 2, 3],
                  y: []
              };
              supplyDefaults(traceIn, traceOut, defaultColor, layout);
              expect(traceOut.visible).toBe(false);
          });
  
          [{letter: 'y', counter: 'x'}, {letter: 'x', counter: 'y'}].forEach(function(spec) {
              var l = spec.letter;
              var c = spec.counter;
              var c0 = c + '0';
              var dc = 'd' + c;
              it('should be visible using ' + c0 + '/' + dc + ' if ' + c + ' is missing completely but ' + l + ' is present', function() {
                  traceIn = {};
                  traceIn[spec.letter] = [1, 2];
                  supplyDefaults(traceIn, traceOut, defaultColor, layout);
                  expect(traceOut.visible).toBe(undefined, l); // visible: true gets set above the module level
                  expect(traceOut._length).toBe(2, l);
                  expect(traceOut[c0]).toBe(0, c0);
                  expect(traceOut[dc]).toBe(1, dc);
              });
          });
  
          it('should correctly assign \'hoveron\' default', function() {
              traceIn = {
                  x: [1, 2, 3],
                  y: [1, 2, 3],
                  mode: 'lines+markers',
                  fill: 'tonext'
              };
  
              // fills and markers, you get both hover types
              // you need visible: true here, as that normally gets set
              // outside of the module supplyDefaults
              traceOut = {visible: true};
              supplyDefaults(traceIn, traceOut, defaultColor, layout);
              expect(traceOut.hoveron).toBe('points+fills');
  
              // but with only lines (or just fill) and fill tonext or toself
              // you get fills
              traceIn.mode = 'lines';
              traceOut = {visible: true};
              supplyDefaults(traceIn, traceOut, defaultColor, layout);
              expect(traceOut.hoveron).toBe('fills');
  
              // with the wrong fill you always get points
              // only area fills default to hoveron points. Vertical or
              // horizontal fills don't have the same physical meaning,
              // they're generally just filling their own slice, so they
              // default to hoveron points.
              traceIn.fill = 'tonexty';
              traceOut = {visible: true};
              supplyDefaults(traceIn, traceOut, defaultColor, layout);
              expect(traceOut.hoveron).toBe('points');
          });
  
          it('should inherit layout.calendar', function() {
              traceIn = {
                  x: [1, 2, 3],
                  y: [1, 2, 3]
              };
              supplyDefaults(traceIn, traceOut, defaultColor, {calendar: 'islamic'});
  
              // we always fill calendar attributes, because it's hard to tell if
              // we're on a date axis at this point.
              expect(traceOut.xcalendar).toBe('islamic');
              expect(traceOut.ycalendar).toBe('islamic');
          });
  
          it('should take its own calendars', function() {
              traceIn = {
                  x: [1, 2, 3],
                  y: [1, 2, 3],
                  xcalendar: 'coptic',
                  ycalendar: 'ethiopian'
              };
              supplyDefaults(traceIn, traceOut, defaultColor, {calendar: 'islamic'});
  
              expect(traceOut.xcalendar).toBe('coptic');
              expect(traceOut.ycalendar).toBe('ethiopian');
          });
  
          describe('selected / unselected attribute containers', function() {
              function _supply(patch) { traceIn = Lib.extendFlat({
                  mode: 'markers',
                  x: [1, 2, 3],
                  y: [2, 1, 2]
              }, patch);
                  traceOut = {visible: true};
                  supplyDefaults(traceIn, traceOut, defaultColor, layout);
              }
  
              it('should fill in [un]selected.marker.opacity default when no other [un]selected is set', function() {
                  _supply({});
                  expect(traceOut.selected.marker.opacity).toBe(1);
                  expect(traceOut.unselected.marker.opacity).toBe(0.2);
  
                  _supply({ marker: {opacity: 0.6} });
                  expect(traceOut.selected.marker.opacity).toBe(0.6);
                  expect(traceOut.unselected.marker.opacity).toBe(0.12);
              });
  
              it('should not fill in [un]selected.marker.opacity default when some other [un]selected is set', function() {
                  _supply({
                      selected: {marker: {size: 20}}
                  });
                  expect(traceOut.selected.marker.opacity).toBeUndefined();
                  expect(traceOut.selected.marker.size).toBe(20);
                  expect(traceOut.unselected).toBeUndefined();
  
                  _supply({
                      unselected: {marker: {color: 'red'}}
                  });
                  expect(traceOut.selected).toBeUndefined();
                  expect(traceOut.unselected.marker.opacity).toBeUndefined();
                  expect(traceOut.unselected.marker.color).toBe('red');
  
                  _supply({
                      mode: 'markers+text',
                      selected: {textfont: {color: 'blue'}}
                  });
                  expect(traceOut.selected.marker).toBeUndefined();
                  expect(traceOut.selected.textfont.color).toBe('blue');
                  expect(traceOut.unselected).toBeUndefined();
              });
          });
  
      });
  
      describe('isBubble', function() {
          it('should return true when marker.size is an Array', function() {
              var trace = {
                      marker: {
                          size: [1, 4, 2, 10]
                      }
                  },
                  isBubble = Scatter.isBubble(trace);
  
              expect(isBubble).toBe(true);
          });
  
          it('should return false when marker.size is an number', function() {
              var trace = {
                      marker: {
                          size: 10
                      }
                  },
                  isBubble = Scatter.isBubble(trace);
  
              expect(isBubble).toBe(false);
          });
  
          it('should return false when marker.size is not defined', function() {
              var trace = {
                      marker: {
                          color: 'red'
                      }
                  },
                  isBubble = Scatter.isBubble(trace);
  
              expect(isBubble).toBe(false);
          });
  
          it('should return false when marker is not defined', function() {
              var trace = {
                      line: {
                          color: 'red'
                      }
                  },
                  isBubble = Scatter.isBubble(trace);
  
              expect(isBubble).toBe(false);
          });
  
      });
  
      describe('makeBubbleSizeFn', function() {
          var markerSizes = [
                  0, '1', 2.21321321, 'not-a-number',
                  100, 1000.213213, 1e7, undefined, null, -100
              ],
              trace = { marker: {} };
  
          var sizeFn, expected;
  
          it('should scale w.r.t. bubble diameter when sizemode=diameter', function() {
              trace.marker.sizemode = 'diameter';
              sizeFn = makeBubbleSizeFn(trace);
  
              expected = [
                  0, 0.5, 1.106606605, 0, 50, 500.1066065, 5000000, 0, 0, 0
              ];
              expect(markerSizes.map(sizeFn)).toEqual(expected);
          });
  
          it('should scale w.r.t. bubble area when sizemode=area', function() {
              trace.marker.sizemode = 'area';
              sizeFn = makeBubbleSizeFn(trace);
  
              expected = [
                  0, 0.7071067811865476, 1.051953708582274, 0, 7.0710678118654755,
                  22.363063441755916, 2236.06797749979, 0, 0, 0
              ];
              expect(markerSizes.map(sizeFn)).toEqual(expected);
          });
  
          it('should adjust scaling according to sizeref', function() {
              trace.marker.sizemode = 'diameter';
              trace.marker.sizeref = 0.1;
              sizeFn = makeBubbleSizeFn(trace);
  
              expected = [
                  0, 5, 11.06606605, 0, 500, 5001.066065, 50000000, 0, 0, 0
              ];
              expect(markerSizes.map(sizeFn)).toEqual(expected);
          });
  
          it('should adjust the small sizes according to sizemin', function() {
              trace.marker.sizemode = 'diameter';
              trace.marker.sizeref = 10;
              trace.marker.sizemin = 5;
              sizeFn = makeBubbleSizeFn(trace);
  
              expected = [
                  0, 5, 5, 0, 5, 50.01066065, 500000, 0, 0, 0
              ];
              expect(markerSizes.map(sizeFn)).toEqual(expected);
          });
      });
  
      describe('linePoints', function() {
          // test axes are unit-scaled and 100 units long
          var ax = {_length: 100, c2p: Lib.identity},
              baseOpts = {
                  xaxis: ax,
                  yaxis: ax,
                  connectGaps: false,
                  baseTolerance: 1,
                  shape: 'linear',
                  simplify: true
              };
  
          function makeCalcData(ptsIn) {
              return ptsIn.map(function(pt) {
                  return {x: pt[0], y: pt[1]};
              });
          }
  
          function callLinePoints(ptsIn, opts) {
              var thisOpts = {};
              if(!opts) opts = {};
              Object.keys(baseOpts).forEach(function(key) {
                  if(opts[key] !== undefined) thisOpts[key] = opts[key];
                  else thisOpts[key] = baseOpts[key];
              });
              return linePoints(makeCalcData(ptsIn), thisOpts);
          }
  
          it('should pass along well-separated non-linear points', function() {
              var ptsIn = [[0, 0], [10, 20], [20, 10], [30, 40], [40, 60], [50, 30]];
              var ptsOut = callLinePoints(ptsIn);
  
              expect(ptsOut).toEqual([ptsIn]);
          });
  
          it('should collapse straight lines to just their endpoints', function() {
              var ptsIn = [[0, 0], [5, 10], [13, 26], [15, 30], [22, 16], [28, 4], [30, 0]];
              var ptsOut = callLinePoints(ptsIn);
              // TODO: [22,16] should not appear here. This is ok but not optimal.
              expect(ptsOut).toEqual([[[0, 0], [15, 30], [22, 16], [30, 0]]]);
          });
  
          it('should not collapse straight lines if simplify is false', function() {
              var ptsIn = [[0, 0], [5, 10], [13, 26], [15, 30], [22, 16], [28, 4], [30, 0]];
              var ptsOut = callLinePoints(ptsIn, {simplify: false});
              expect(ptsOut).toEqual([ptsIn]);
          });
  
          it('should separate out blanks, unless connectgaps is true', function() {
              var ptsIn = [
                  [0, 0], [10, 20], [20, 10], [undefined, undefined],
                  [30, 40], [undefined, undefined],
                  [40, 60], [50, 30]];
              var ptsDisjoint = callLinePoints(ptsIn);
              var ptsConnected = callLinePoints(ptsIn, {connectGaps: true});
  
              expect(ptsDisjoint).toEqual([[[0, 0], [10, 20], [20, 10]], [[30, 40]], [[40, 60], [50, 30]]]);
              expect(ptsConnected).toEqual([[[0, 0], [10, 20], [20, 10], [30, 40], [40, 60], [50, 30]]]);
          });
  
          it('should collapse a vertical cluster into 4 points', function() {
              // the four being initial, high, low, and final if the high is before the low
              var ptsIn = [[-10, 0], [0, 0], [0, 10], [0, 20], [0, -10], [0, 15], [0, -25], [0, 10], [0, 5], [10, 10]];
              var ptsOut = callLinePoints(ptsIn);
  
              // TODO: [0, 10] should not appear in either of these results - this is OK but not optimal.
              expect(ptsOut).toEqual([[[-10, 0], [0, 0], [0, 10], [0, 20], [0, -25], [0, 5], [10, 10]]]);
  
              // or initial, low, high, final if the low is before the high
              ptsIn = [[-10, 0], [0, 0], [0, 10], [0, -25], [0, -10], [0, 15], [0, 20], [0, 10], [0, 5], [10, 10]];
              ptsOut = callLinePoints(ptsIn);
  
              expect(ptsOut).toEqual([[[-10, 0], [0, 0], [0, 10], [0, -25], [0, 20], [0, 5], [10, 10]]]);
          });
  
          it('should collapse a horizontal cluster into 4 points', function() {
              // same deal
              var ptsIn = [[0, -10], [0, 0], [10, 0], [20, 0], [-10, 0], [15, 0], [-25, 0], [10, 0], [5, 0], [10, 10]];
              var ptsOut = callLinePoints(ptsIn);
  
              // TODO: [10, 0] should not appear in either of these results - this is OK but not optimal.
              // same problem as the test above
              expect(ptsOut).toEqual([[[0, -10], [0, 0], [10, 0], [20, 0], [-25, 0], [5, 0], [10, 10]]]);
  
              ptsIn = [[0, -10], [0, 0], [10, 0], [-25, 0], [-10, 0], [15, 0], [20, 0], [10, 0], [5, 0], [10, 10]];
              ptsOut = callLinePoints(ptsIn);
  
              expect(ptsOut).toEqual([[[0, -10], [0, 0], [10, 0], [-25, 0], [20, 0], [5, 0], [10, 10]]]);
          });
  
          it('should use lineWidth to determine whether a cluster counts', function() {
              var ptsIn = [[0, 0], [20, 0], [21, 10], [22, 20], [23, -10], [24, 15], [25, -25], [26, 10], [27, 5], [100, 10]];
              var ptsThin = callLinePoints(ptsIn);
              var ptsThick = callLinePoints(ptsIn, {baseTolerance: 8});
  
              // thin line, no decimation. thick line yes.
              expect(ptsThin).toEqual([ptsIn]);
              // TODO: [21,10] should not appear in this result (same issue again)
              expect(ptsThick).toEqual([[[0, 0], [20, 0], [21, 10], [22, 20], [25, -25], [27, 5], [100, 10]]]);
          });
  
          // TODO: test coarser decimation outside plot, and removing very near duplicates from the four of a cluster
  
          function reverseXY(v) { return [v[1], v[0]]; }
  
          function reverseXY2(v) { return v.map(reverseXY); }
  
          it('should clip extreme points without changing on-screen paths', function() {
              var ptsIn = [
                  // first bunch: rays going in/out in many directions
                  // and a few random moves within faraway sectors, that should get dropped
                  // for simplicity of calculation all are 45 degree multiples, but not exactly on corners
                  [[40, 70], [40, 1000000], [-100, 2000000], [200, 2000000], [60, 3000000], [60, 70]],
                  // back and forth across the diagonal
                  [[60, 70], [1000060, 1000070], [-2000070, -2000060], [-3000060, -3000070], [10000070, 10000060], [70, 60]],
                  [[70, 60], [1000000, 60], [100000, 50], [60, 50]],
                  [[60, 50], [1000110, -1000000], [10000100, -10000010], [50, 40]],
                  // back and forth across the vertical
                  [[50, 40], [50, -3000000], [49, -3000000], [49, 4000000], [48, 3000000], [48, -4000000], [40, -1000000], [40, 30]],
                  [[40, 30], [-1000000, -1000010], [-2000010, -2000000], [30, 40]],
                  // back and forth across the horizontal
                  [[30, 40], [-5000000, 40], [-900000, -500], [-1000000, 50], [1000000, 50], [-2000000, 50], [40, 50]],
                  [[40, 50], [-1000010, 1000100], [-2000000, 2000100], [50, 60]],
  
                  // some paths crossing the nearby region in various ways
                  [[0, 3100], [-20000, -36900], [20000, -36900], [0, 3100]],
                  [[0, -3000], [-20000, 37000], [20000, 37000], [0, -3000]],
  
                  // loops around the outside
                  [[55, 1000000], [2000000, 23], [444, -3000000], [-4000000, 432], [-22, 5000000]],
                  [[12, 1000000], [2000000, 1000000], [3000000, -4000000], [-5000000, -6000000], [-7000000, 8000000], [-13, 9000000]],
  
                  // wound-unwound loop
                  [[55, -100000], [100000, 0], [0, 100000], [-100000, 0], [0, -100000], [-1000000, 100000], [1000000, 100000], [66, -100000]],
  
                  // outside kitty-corner
                  [[1e5, 1e6], [-1e6, -1e5], [-1e6, 1e5], [1e5, -1e6], [-1e5, -1e6], [1e6, 1e5]]
              ];
  
              var ptsExpected = [
                  [[40, 70], [40, 2100], [60, 2100], [60, 70]],
                  [[60, 70], [2090, 2100], [-2000, -1990], [-2000, -2000], [-1990, -2000], [2100, 2090], [70, 60]],
                  [[70, 60], [2100, 60], [2100, 50], [60, 50]],
                  [[60, 50], [2100, -1990], [2100, -2000], [2090, -2000], [50, 40]],
                  [[50, 40], [50, -2000], [49, -2000], [49, 2100], [48, 2100], [48, -2000], [40, -2000], [40, 30]],
                  [[40, 30], [-1990, -2000], [-2000, -2000], [-2000, -1990], [30, 40]],
                  [[30, 40], [-2000, 40], [-2000, 50], [2100, 50], [-2000, 50], [40, 50]],
                  [[40, 50], [-2000, 2090], [-2000, 2100], [-1990, 2100], [50, 60]],
  
                  [[0, 2100], [-500, 2100], [-2000, -900], [-2000, -2000], [2100, -2000], [2100, -1100], [500, 2100], [0, 2100]],
                  [[0, -2000], [-500, -2000], [-2000, 1000], [-2000, 2100], [2100, 2100], [2100, 1200], [500, -2000], [0, -2000]],
  
                  [[55, 2100], [2100, 2100], [2100, -2000], [-2000, -2000], [-2000, 2100], [-22, 2100]],
                  [[12, 2100], [2100, 2100], [2100, -2000], [-2000, -2000], [-2000, 2100], [-13, 2100]],
  
                  [[55, -2000], [66, -2000]],
  
                  [[2100, 2100], [-2000, 2100], [-2000, -2000], [2100, -2000], [2100, 2100]]
              ];
  
              ptsIn.forEach(function(ptsIni, i) {
                  // disable clustering for these tests
                  var ptsOut = callLinePoints(ptsIni, {simplify: false});
                  expect(ptsOut.length).toBe(1, i);
                  expect(ptsOut[0]).toBeCloseTo2DArray(ptsExpected[i], 1, i);
  
                  // swap X and Y and all should work identically
                  var ptsOut2 = callLinePoints(ptsIni.map(reverseXY), {simplify: false});
                  expect(ptsOut2.length).toBe(1, i);
                  expect(ptsOut2[0]).toBeCloseTo2DArray(ptsExpected[i].map(reverseXY), 1, i);
              });
          });
  
          it('works when far off-screen points cross the viewport', function() {
              function _check(ptsIn, ptsExpected) {
                  var ptsOut = callLinePoints(ptsIn);
                  expect(JSON.stringify(ptsOut)).toEqual(JSON.stringify([ptsExpected]));
  
                  var ptsOut2 = callLinePoints(ptsIn.map(reverseXY)).map(reverseXY2);
                  expect(JSON.stringify(ptsOut2)).toEqual(JSON.stringify([ptsExpected]));
              }
  
              // first cross the viewport horizontally/vertically
              _check([
                  [-822, 20], [-802, 2], [-801.5, 1.1], [-800, 0],
                  [900, 0], [901.5, 1.1], [902, 2], [922, 20]
              ], [
                  // all that's really important here (and the next check) is that
                  // the points [-800, 0] and [900, 0] are connected. What we do
                  // with other points beyond those doesn't matter too much.
                  [-822, 20], [-800, 0],
                  [900, 0], [922, 20]
              ]);
  
              _check([
                  [-804, 4], [-802, 2], [-801.5, 1.1], [-800, 0],
                  [900, 0], [901.5, 1.1], [902, 2], [904, 4]
              ], [
                  [-804, 4], [-800, 0],
                  [900, 0]
              ]);
  
              // now cross the viewport diagonally
              _check([
                  [-801, 925], [-800, 902], [-800.5, 901.1], [-800, 900],
                  [900, -800], [900.5, -801.1], [900, -802], [901, -825]
              ], [
                  // similarly here, we just care that
                  // [-800, 900] connects to [900, -800]
                  [-801, 925], [-800, 900],
                  [900, -800], [901, -825]
              ]);
          });
      });
  
  });
  
  describe('end-to-end scatter tests', function() {
      var gd;
  
      beforeEach(function() {
          gd = createGraphDiv();
      });
  
      afterEach(destroyGraphDiv);
  
      it('should add a plotly-customdata class to points with custom data', function(done) {
          Plotly.plot(gd, [{
              x: [1, 2, 3, 4, 5, 6, 7],
              y: [2, 3, 4, 5, 6, 7, 8],
              customdata: [null, undefined, 0, false, {foo: 'bar'}, 'a']
          }]).then(function() {
              var points = d3.selectAll('g.scatterlayer').selectAll('.point');
  
              // Rather than just duplicating the logic, let's be explicit about
              // what's expected. Specifially, only null and undefined (the default)
              // do *not* add the class.
              var expected = [false, false, true, true, true, true, false];
  
              points.each(function(cd, i) {
                  expect(d3.select(this).classed('plotly-customdata')).toBe(expected[i]);
              });
  
              return Plotly.animate(gd, [{
                  data: [{customdata: []}]
              }], {frame: {redraw: false, duration: 0}});
          }).then(function() {
              var points = d3.selectAll('g.scatterlayer').selectAll('.point');
  
              points.each(function() {
                  expect(d3.select(this).classed('plotly-customdata')).toBe(false);
              });
  
          }).catch(fail).then(done);
      });
  
      it('adds "textpoint" class to scatter text points', function(done) {
          Plotly.plot(gd, [{
              mode: 'text',
              x: [1, 2, 3],
              y: [2, 3, 4],
              text: ['a', 'b', 'c']
          }]).then(function() {
              expect(Plotly.d3.selectAll('.textpoint').size()).toBe(3);
          }).catch(fail).then(done);
      });
  
      it('should remove all point and text nodes on blank data', function(done) {
          function assertNodeCnt(ptCnt, txCnt) {
              expect(d3.selectAll('.point').size()).toEqual(ptCnt);
              expect(d3.selectAll('.textpoint').size()).toEqual(txCnt);
          }
  
          function assertText(content) {
              d3.selectAll('.textpoint').each(function(_, i) {
                  var tx = d3.select(this).select('text');
                  expect(tx.text()).toEqual(content[i]);
              });
          }
  
          Plotly.plot(gd, [{
              x: [150, 350, 650],
              y: [100, 300, 600],
              text: ['A', 'B', 'C'],
              mode: 'markers+text',
              marker: {
                  size: [100, 200, 300],
                  line: { width: [10, 20, 30] },
                  color: 'yellow',
                  sizeref: 3,
                  gradient: {
                      type: 'radial',
                      color: 'white'
                  }
              }
          }])
          .then(function() {
              assertNodeCnt(3, 3);
              assertText(['A', 'B', 'C']);
  
              return Plotly.restyle(gd, {
                  x: [[null, undefined, NaN]],
                  y: [[NaN, null, undefined]]
              });
          })
          .then(function() {
              assertNodeCnt(0, 0);
  
              return Plotly.restyle(gd, {
                  x: [[150, 350, 650]],
                  y: [[100, 300, 600]]
              });
          })
          .then(function() {
              assertNodeCnt(3, 3);
              assertText(['A', 'B', 'C']);
          })
          .catch(fail)
          .then(done);
      });
  
      function _assertNodes(ptStyle, txContent) {
          var pts = d3.selectAll('.point');
          var txs = d3.selectAll('.textpoint');
  
          expect(pts.size()).toEqual(ptStyle.length);
          expect(txs.size()).toEqual(txContent.length);
  
          pts.each(function(_, i) {
              expect(this.style.fill).toEqual(ptStyle[i], 'pt ' + i);
          });
  
          txs.each(function(_, i) {
              expect(d3.select(this).select('text').text()).toEqual(txContent[i], 'tx ' + i);
          });
      }
  
      it('should reorder point and text nodes even when linked to ids (shuffle case)', function(done) {
          Plotly.plot(gd, [{
              x: [150, 350, 650],
              y: [100, 300, 600],
              text: ['apple', 'banana', 'clementine'],
              ids: ['A', 'B', 'C'],
              mode: 'markers+text',
              marker: {
                  color: ['rgb(255, 0, 0)', 'rgb(0, 255, 0)', 'rgb(0, 0, 255)']
              },
              transforms: [{
                  type: 'sort',
                  enabled: false,
                  target: [0, 1, 0]
              }]
          }])
          .then(function() {
              _assertNodes(
                  ['rgb(255, 0, 0)', 'rgb(0, 255, 0)', 'rgb(0, 0, 255)'],
                  ['apple', 'banana', 'clementine']
              );
  
              return Plotly.restyle(gd, 'transforms[0].enabled', true);
          })
          .then(function() {
              _assertNodes(
                  ['rgb(255, 0, 0)', 'rgb(0, 0, 255)', 'rgb(0, 255, 0)'],
                  ['apple', 'clementine', 'banana']
              );
  
              return Plotly.restyle(gd, 'transforms[0].enabled', false);
          })
          .then(function() {
              _assertNodes(
                  ['rgb(255, 0, 0)', 'rgb(0, 255, 0)', 'rgb(0, 0, 255)'],
                  ['apple', 'banana', 'clementine']
              );
          })
          .catch(fail)
          .then(done);
      });
  
      it('should reorder point and text nodes even when linked to ids (add/remove case)', function(done) {
          Plotly.plot(gd, [{
              x: [150, 350, null, 600],
              y: [100, 300, null, 700],
              text: ['apple', 'banana', null, 'clementine'],
              ids: ['A', 'B', null, 'C'],
              mode: 'markers+text',
              marker: {
                  color: ['rgb(255, 0, 0)', 'rgb(0, 255, 0)', null, 'rgb(0, 0, 255)']
              },
              transforms: [{
                  type: 'filter',
                  enabled: false,
                  target: [1, 0, 0, 1],
                  operation: '=',
                  value: 1
              }]
          }])
          .then(function() {
              _assertNodes(
                  ['rgb(255, 0, 0)', 'rgb(0, 255, 0)', 'rgb(0, 0, 255)'],
                  ['apple', 'banana', 'clementine']
              );
  
              return Plotly.restyle(gd, 'transforms[0].enabled', true);
          })
          .then(function() {
              _assertNodes(
                  ['rgb(255, 0, 0)', 'rgb(0, 0, 255)'],
                  ['apple', 'clementine']
              );
  
              return Plotly.restyle(gd, 'transforms[0].enabled', false);
          })
          .then(function() {
              _assertNodes(
                  ['rgb(255, 0, 0)', 'rgb(0, 255, 0)', 'rgb(0, 0, 255)'],
                  ['apple', 'banana', 'clementine']
              );
          })
          .catch(fail)
          .then(done);
      });
  
      it('should smoothly add/remove nodes tags with *ids* during animations', function(done) {
          Plotly.plot(gd, {
              data: [{
                  mode: 'markers+text',
                  y: [1, 2, 1],
                  text: ['apple', 'banana', 'clementine'],
                  ids: ['A', 'B', 'C'],
                  marker: { color: ['red', 'green', 'blue'] }
              }],
              frames: [{
                  data: [{
                      y: [2, 1, 2],
                      text: ['apple', 'banana', 'dragon fruit'],
                      ids: ['A', 'C', 'D'],
                      marker: { color: ['red', 'blue', 'yellow'] }
                  }]
              }]
          })
          .then(function() {
              _assertNodes(
                  ['rgb(255, 0, 0)', 'rgb(0, 128, 0)', 'rgb(0, 0, 255)'],
                  ['apple', 'banana', 'clementine']
              );
  
              return Plotly.animate(gd, null, {frame: {redraw: false}});
          })
          .then(function() {
              _assertNodes(
                  ['rgb(255, 0, 0)', 'rgb(0, 0, 255)', 'rgb(255, 255, 0)'],
                  ['apple', 'banana', 'dragon fruit']
              );
          })
          .catch(fail)
          .then(done);
      });
  
      it('animates fillcolor', function(done) {
          function fill() {
              return d3.selectAll('.js-fill').node().style.fill;
          }
  
          Plotly.plot(gd, [{
              x: [1, 2, 3, 4, 5, 6, 7],
              y: [2, 3, 4, 5, 6, 7, 8],
              fill: 'tozeroy',
              fillcolor: 'rgb(255, 0, 0)',
          }]).then(function() {
              expect(fill()).toEqual('rgb(255, 0, 0)');
              return Plotly.animate(gd,
                  [{data: [{fillcolor: 'rgb(0, 0, 255)'}]}],
                  {frame: {duration: 0, redraw: false}}
              );
          }).then(function() {
              expect(fill()).toEqual('rgb(0, 0, 255)');
          }).catch(fail).then(done);
      });
  
      it('clears fills tonext when either trace is emptied out', function(done) {
          var trace0 = {y: [1, 3, 5, 7]};
          var trace1 = {y: [1, 2, 3, 4], line: {width: 0}, mode: 'lines'};
          var trace2 = {y: [3, 4, 5, 6], fill: 'tonexty', mode: 'none'};
  
          function checkFill(visible, msg) {
              var fillSelection = d3.select(gd).selectAll('.scatterlayer .js-fill');
              expect(fillSelection.size()).toBe(1, msg);
              expect(fillSelection.attr('d')).negateIf(visible).toBe('M0,0Z', msg);
          }
  
          Plotly.newPlot(gd, [trace0, trace1, trace2], {}, {scrollZoom: true})
          .then(function() {
              checkFill(true, 'initial');
  
              return Plotly.restyle(gd, {y: [[null, null, null, null]]}, [1]);
          })
          .then(function() {
              checkFill(false, 'null out trace 1');
  
              return Plotly.restyle(gd, {y: [[1, 2, 3, 4]]}, [1]);
          })
          .then(function() {
              checkFill(true, 'restore trace 1');
  
              return Plotly.restyle(gd, {y: [[null, null, null, null]]}, [2]);
          })
          .then(function() {
              checkFill(false, 'null out trace 2');
  
              return Plotly.restyle(gd, {y: [[1, 2, 3, 4]]}, [2]);
          })
          .then(function() {
              checkFill(true, 'restore trace 2');
  
              return Plotly.restyle(gd, {y: [[null, null, null, null], [null, null, null, null]]}, [1, 2]);
          })
          .then(function() {
              checkFill(false, 'null out both traces');
          })
          .catch(fail)
          .then(done);
      });
  
      it('should work with typed arrays', function(done) {
          function _assert(colors, sizes) {
              var pts = d3.selectAll('.point');
              expect(pts.size()).toBe(3, '# of pts');
  
              pts.each(function(_, i) {
                  expect(getColor(this)).toBe(colors[i], 'color ' + i);
                  expect(getMarkerSize(this)).toBe(sizes[i], 'size ' + i);
              });
          }
  
          Plotly.newPlot(gd, [{
              x: new Float32Array([1, 2, 3]),
              y: new Float32Array([1, 2, 1]),
              marker: {
                  size: new Float32Array([20, 30, 10]),
                  color: new Float32Array([10, 30, 20]),
                  cmin: 10,
                  cmax: 30,
                  colorscale: [
                      [0, 'rgb(255, 0, 0)'],
                      [0.5, 'rgb(0, 255, 0)'],
                      [1, 'rgb(0, 0, 255)']
                  ]
              }
          }])
          .then(function() {
              _assert(
                  ['rgb(255, 0, 0)', 'rgb(0, 0, 255)', 'rgb(0, 255, 0)'],
                  [20, 30, 10]
              );
  
              return Plotly.restyle(gd, {
                  'marker.size': [new Float32Array([40, 30, 20])],
                  'marker.color': [new Float32Array([20, 30, 10])]
              });
          })
          .then(function() {
              _assert(
                  ['rgb(0, 255, 0)', 'rgb(0, 0, 255)', 'rgb(255, 0, 0)'],
                  [40, 30, 20]
              );
          })
          .catch(fail)
          .then(done);
      });
  });
  
  describe('scatter hoverPoints', function() {
  
      afterEach(destroyGraphDiv);
  
      function _hover(gd, xval, yval, hovermode) {
          return gd._fullData.map(function(trace, i) {
              var cd = gd.calcdata[i];
              var subplot = gd._fullLayout._plots.xy;
  
              var out = Scatter.hoverPoints({
                  index: false,
                  distance: 20,
                  cd: cd,
                  trace: trace,
                  xa: subplot.xaxis,
                  ya: subplot.yaxis
              }, xval, yval, hovermode);
  
              return Array.isArray(out) ? out[0] : null;
          });
      }
  
      it('should show \'hovertext\' items when present, \'text\' if not', function(done) {
          var gd = createGraphDiv();
          var mock = Lib.extendDeep({}, require('@mocks/text_chart_arrays'));
  
          Plotly.plot(gd, mock).then(function() {
              var pts = _hover(gd, 0, 1, 'x');
  
              // as in 'hovertext' arrays
              expect(pts[0].text).toEqual('Hover text\nA', 'hover text');
              expect(pts[1].text).toEqual('Hover text G', 'hover text');
              expect(pts[2].text).toEqual('a (hover)', 'hover text');
  
              return Plotly.restyle(gd, 'hovertext', null);
          })
          .then(function() {
              var pts = _hover(gd, 0, 1, 'x');
  
              // as in 'text' arrays
              expect(pts[0].text).toEqual('Text\nA', 'hover text');
              expect(pts[1].text).toEqual('Text G', 'hover text');
              expect(pts[2].text).toEqual('a', 'hover text');
  
              return Plotly.restyle(gd, 'text', ['APPLE', 'BANANA', 'ORANGE']);
          })
          .then(function() {
              var pts = _hover(gd, 1, 1, 'x');
  
              // as in 'text' values
              expect(pts[0].text).toEqual('APPLE', 'hover text');
              expect(pts[1].text).toEqual('BANANA', 'hover text');
              expect(pts[2].text).toEqual('ORANGE', 'hover text');
  
              return Plotly.restyle(gd, 'hovertext', ['apple', 'banana', 'orange']);
          })
          .then(function() {
              var pts = _hover(gd, 1, 1, 'x');
  
              // as in 'hovertext' values
              expect(pts[0].text).toEqual('apple', 'hover text');
              expect(pts[1].text).toEqual('banana', 'hover text');
              expect(pts[2].text).toEqual('orange', 'hover text');
          })
          .catch(fail)
          .then(done);
      });
  });
  
  describe('Test Scatter.style', function() {
      var gd;
  
      beforeEach(function() {
          gd = createGraphDiv();
      });
  
      afterEach(destroyGraphDiv);
  
      function assertPts(attr, getterFn, expectation, msg2) {
          var selector = attr.indexOf('textfont') === 0 ? '.textpoint > text' : '.point';
  
          d3.select(gd).selectAll('.trace').each(function(_, i) {
              var pts = d3.select(this).selectAll(selector);
              var expi = expectation[i];
  
              expect(pts.size())
                  .toBe(expi.length, '# of pts for trace ' + i + msg2);
  
              pts.each(function(_, j) {
                  var msg3 = ' for pt ' + j + ' in trace ' + i + msg2;
                  expect(getterFn(this)).toBe(expi[j], attr + msg3);
              });
          });
      }
  
      function makeCheckFn(attr, getterFn) {
          return function(update, expectation, msg) {
              var promise = update ? Plotly.restyle(gd, update) : Promise.resolve();
  
              return promise.then(function() {
                  assertPts(attr, getterFn, expectation, ' (' + msg + ' after restyle)');
  
                  // make sure styleOnSelect (called during selection)
                  // gives same results as restyle
                  gd.calcdata.forEach(function(cd) {
                      Scatter.styleOnSelect(gd, cd);
                  });
                  assertPts(attr, getterFn, expectation, ' (' + msg + ' via Scatter.styleOnSelect)');
              });
          };
      }
  
      var r = 'rgb(255, 0, 0)';
      var g = 'rgb(0, 255, 0)';
      var b = 'rgb(0, 0, 255)';
      var y = 'rgb(255, 255, 0)';
      var c = 'rgb(0, 255, 255)';
  
      it('should style selected point marker opacity correctly', function(done) {
          var check = makeCheckFn('marker.opacity', getOpacity);
  
          Plotly.plot(gd, [{
              mode: 'markers',
              y: [1, 2, 1],
              marker: {opacity: 0.6}
          }, {
              mode: 'markers',
              y: [2, 1, 2],
              marker: {opacity: [0.5, 0.5, 0.5]}
          }])
          .then(function() {
              return check(
                  null,
                  [[0.6, 0.6, 0.6], [0.5, 0.5, 0.5]],
                  'base case'
              );
          })
          .then(function() {
              return check(
                  {selectedpoints: [[1]]},
                  [[0.12, 0.6, 0.12], [0.1, 0.5, 0.1]],
                  'selected pt 1 w/o [un]selected setting'
              );
          })
          .then(function() {
              return check(
                  {'selected.marker.opacity': 1},
                  [[0.12, 1, 0.12], [0.1, 1, 0.1]],
                  'selected pt 1 w/ set selected.marker.opacity'
              );
          })
          .then(function() {
              return check(
                  {selectedpoints: [[1, 2]]},
                  [[0.12, 1, 1], [0.1, 1, 1]],
                  'selected pt 1-2 w/ set selected.marker.opacity'
              );
          })
          .then(function() {
              return check(
                  {selectedpoints: [[2]]},
                  [[0.12, 0.12, 1], [0.1, 0.1, 1]],
                  'selected pt 2 w/ set selected.marker.opacity'
              );
          })
          .then(function() {
              return check(
                  {selectedpoints: null},
                  [[0.6, 0.6, 0.6], [0.5, 0.5, 0.5]],
                  'no selected pts w/ set selected.marker.opacity'
              );
          })
          .then(function() {
              return check(
                  {selectedpoints: [[1]]},
                  [[0.12, 1, 0.12], [0.1, 1, 0.1]],
                  'selected pt 1 w/o [un]selected setting (take 2)'
              );
          })
          .then(function() {
              return check(
                  {'unselected.marker.opacity': 0},
                  [[0, 1, 0], [0, 1, 0]],
                  'selected pt 1 w/ set [un]selected.marker.opacity'
              );
          })
          .then(function() {
              return check(
                  {'selected.marker.opacity': null},
                  [[0, 0.6, 0], [0, 0.5, 0]],
                  'selected pt 1 w/ set unselected.marker.opacity'
              );
          })
          .catch(fail)
          .then(done);
      });
  
      it('should style selected point marker color correctly', function(done) {
          var check = makeCheckFn('marker.color', getColor);
          var checkOpacity = makeCheckFn('marker.opacity', getOpacity);
  
          Plotly.plot(gd, [{
              mode: 'markers',
              y: [1, 2, 1],
              marker: {color: b}
          }, {
              mode: 'markers',
              y: [2, 1, 2],
              marker: {color: [r, g, b]}
          }])
          .then(function() {
              return check(
                  null,
                  [[b, b, b], [r, g, b]],
                  'base case'
              );
          })
          .then(function() {
              return check(
                  {selectedpoints: [[0, 2]]},
                  [[b, b, b], [r, g, b]],
                  'selected pts 0-2 w/o [un]selected setting'
              );
          })
          .then(function() {
              return checkOpacity(
                  null,
                  [[1, 0.2, 1], [1, 0.2, 1]],
                  'selected pts 0-2 w/o [un]selected setting [should just change opacity]'
              );
          })
          .then(function() {
              return check(
                  {'selected.marker.color': y},
                  [[y, b, y], [y, g, y]],
                  'selected pts 0-2 w/ set selected.marker.color'
              );
          })
          .then(function() {
              return checkOpacity(
                  null,
                  [[1, 1, 1], [1, 1, 1]],
                  'selected pts 0-2 w/o [un]selected setting [should NOT change opacity]'
              );
          })
          .then(function() {
              return check(
                  {selectedpoints: [[1, 2]]},
                  [[b, y, y], [r, y, y]],
                  'selected pt 1-2 w/ set selected.marker.color'
              );
          })
          .then(function() {
              return check(
                  {selectedpoints: null},
                  [[b, b, b], [r, g, b]],
                  'no selected pts w/ set selected.marker.color'
              );
          })
          .then(function() {
              return check(
                  {selectedpoints: [[0, 2]]},
                  [[y, b, y], [y, g, y]],
                  'selected pts 0-2 w/ set selected.marker.color (take 2)'
              );
          })
          .then(function() {
              return check(
                  {'unselected.marker.color': c},
                  [[y, c, y], [y, c, y]],
                  'selected pts 0-2 w/ set [un]selected.marker.color'
              );
          })
          .then(function() {
              return check(
                  {'selected.marker.color': null},
                  [[b, c, b], [r, c, b]],
                  'selected pts 0-2 w/ set selected.marker.color'
              );
          })
          .catch(fail)
          .then(done);
      });
  
      it('should style selected point marker size correctly', function(done) {
          var check = makeCheckFn('marker.size', getMarkerSize);
  
          Plotly.plot(gd, [{
              mode: 'markers',
              y: [1, 2, 1],
              marker: {size: 20}
          }, {
              mode: 'markers',
              y: [2, 1, 2],
              marker: {size: [15, 25, 35]}
          }])
          .then(function() {
              return check(
                  null,
                  [[20, 20, 20], [15, 25, 35]],
                  'base case'
              );
          })
          .then(function() {
              return check(
                  {selectedpoints: [[0]], 'selected.marker.size': 40},
                  [[40, 20, 20], [40, 25, 35]],
                  'selected pt 0 w/ set selected.marker.size'
              );
          })
          .then(function() {
              return check(
                  {'unselected.marker.size': 0},
                  [[40, 0, 0], [40, 0, 0]],
                  'selected pt 0 w/ set [un]selected.marker.size'
              );
          })
          .then(function() {
              return check(
                  {'selected.marker.size': null},
                  [[20, 0, 0], [15, 0, 0]],
                  'selected pt 0 w/ set unselected.marker.size'
              );
          })
          .catch(fail)
          .then(done);
      });
  
      it('should style selected point textfont correctly', function(done) {
          var checkFontColor = makeCheckFn('textfont.color', getColor);
          var checkFontOpacity = makeCheckFn('textfont.color (alpha channel)', getFillOpacity);
          var checkPtOpacity = makeCheckFn('marker.opacity', getOpacity);
  
          Plotly.plot(gd, [{
              mode: 'markers+text',
              y: [1, 2, 1],
              text: 'TEXT',
              textfont: {color: b}
          }, {
              mode: 'markers+text',
              y: [2, 1, 2],
              text: ['A', 'B', 'C'],
              textfont: {color: [r, g, b]}
          }])
          .then(function() {
              return checkFontColor(
                  null,
                  [[b, b, b], [r, g, b]],
                  'base case'
              );
          })
          .then(function() {
              return checkFontColor(
                  {selectedpoints: [[0, 2]]},
                  [[b, b, b], [r, g, b]],
                  'selected pts 0-2 w/o [un]selected setting'
              );
          })
          .then(function() {
              return checkFontOpacity(
                  null,
                  [[1, 0.2, 1], [1, 0.2, 1]],
                  'selected pts 0-2 w/o [un]selected setting [should change font color alpha]'
              );
          })
          .then(function() {
              return checkPtOpacity(
                  null,
                  [[1, 0.2, 1], [1, 0.2, 1]],
                  'selected pts 0-2 w/o [un]selected setting [should change pt opacity]'
              );
          })
          .then(function() {
              return checkFontColor(
                  {'selected.textfont.color': y},
                  [[y, b, y], [y, g, y]],
                  'selected pts 0-2 w/ set selected.textfont.color'
              );
          })
          .then(function() {
              return checkFontOpacity(
                  null,
                  [[1, 1, 1], [1, 1, 1]],
                  'selected pts 0-2 w set selected.textfont.color [should NOT change font color alpha]'
              );
          })
          .then(function() {
              return checkPtOpacity(
                  null,
                  [[1, 1, 1], [1, 1, 1]],
                  'selected pts 0-2 w/o [un]selected setting [should NOT change opacity]'
              );
          })
          .then(function() {
              return checkFontColor(
                  {'unselected.textfont.color': c},
                  [[y, c, y], [y, c, y]],
                  'selected pts 0-2 w/ set [un]selected.textfont.color'
              );
          })
          .then(function() {
              return checkFontColor(
                  {'selected.textfont.color': null},
                  [[b, c, b], [r, c, b]],
                  'selected pts 0-2 w/ set selected.textfont.color'
              );
          })
          .catch(fail)
          .then(done);
      });
  });
  
  describe('Test scatter *clipnaxis*:', function() {
      afterEach(destroyGraphDiv);
  
      it('should show/hide point/text/errorbars in clipped and non-clipped layers', function(done) {
          var gd = createGraphDiv();
          var fig = Lib.extendDeep({}, require('@mocks/cliponaxis_false.json'));
          var xRange0 = fig.layout.xaxis.range.slice();
          var yRange0 = fig.layout.yaxis.range.slice();
  
          // only show 1 *cliponaxis: false* trace
          fig.data = [fig.data[2]];
  
          // add lines
          fig.data[0].mode = 'markers+lines+text';
  
          // add a non-scatter trace to make sure its module layer gets clipped
          fig.data.push({
              type: 'contour',
              z: [[0, 0.5, 1], [0.5, 1, 3]]
          });
  
          function _assertClip(sel, exp, size, msg) {
              if(exp === null) {
                  expect(sel.size()).toBe(0, msg + 'selection should not exist');
              } else {
                  assertClip(sel, exp, size, msg);
              }
          }
  
          function _assert(layerClips, nodeDisplays, errorBarClips, lineClips) {
              var subplotLayer = d3.select('.plot');
              var scatterLayer = subplotLayer.select('.scatterlayer');
  
              _assertClip(subplotLayer, layerClips[0], 1, 'subplot layer');
              _assertClip(subplotLayer.select('.contourlayer'), layerClips[1], 1, 'some other trace layer');
              _assertClip(scatterLayer, layerClips[2], 1, 'scatter layer');
  
              assertNodeDisplay(
                  scatterLayer.selectAll('.point'),
                  nodeDisplays,
                  'scatter points'
              );
              assertNodeDisplay(
                  scatterLayer.selectAll('.textpoint'),
                  nodeDisplays,
                  'scatter text points'
              );
  
              assertClip(
                  scatterLayer.selectAll('.errorbar'),
                  errorBarClips[0], errorBarClips[1],
                  'error bars'
              );
              assertClip(
                  scatterLayer.selectAll('.js-line'),
                  lineClips[0], lineClips[1],
                  'line clips'
              );
          }
  
          Plotly.plot(gd, fig)
          .then(function() {
              _assert(
                  [false, true, false],
                  [null, null, null, null, null, null],
                  [true, 6],
                  [true, 1]
              );
              return Plotly.restyle(gd, 'visible', false);
          })
          .then(function() {
              _assert(
                  [true, null, null],
                  [],
                  [false, 0],
                  [false, 0]
              );
              return Plotly.restyle(gd, {visible: true, cliponaxis: null});
          })
          .then(function() {
              _assert(
                  [true, false, false],
                  [null, null, null, null, null, null],
                  [false, 6],
                  [false, 1]
              );
              return Plotly.restyle(gd, 'visible', 'legendonly');
          })
          .then(function() {
              _assert(
                  [true, null, null],
                  [],
                  [false, 0],
                  [false, 0]
              );
              return Plotly.restyle(gd, 'visible', true);
          })
          .then(function() {
              _assert(
                  [true, false, false],
                  [null, null, null, null, null, null],
                  [false, 6],
                  [false, 1]
              );
              return Plotly.restyle(gd, 'cliponaxis', false);
          })
          .then(function() {
              _assert(
                  [false, true, false],
                  [null, null, null, null, null, null],
                  [true, 6],
                  [true, 1]
              );
              return Plotly.relayout(gd, 'xaxis.range', [0, 1]);
          })
          .then(function() {
              _assert(
                  [false, true, false],
                  [null, null, 'none', 'none', 'none', 'none'],
                  [true, 6],
                  [true, 1]
              );
              return Plotly.relayout(gd, 'yaxis.range', [0, 1]);
          })
          .then(function() {
              _assert(
                  [false, true, false],
                  ['none', null, 'none', 'none', 'none', 'none'],
                  [true, 6],
                  [true, 1]
              );
              return Plotly.relayout(gd, {'xaxis.range': xRange0, 'yaxis.range': yRange0});
          })
          .then(function() {
              _assert(
                  [false, true, false],
                  [null, null, null, null, null, null],
                  [true, 6],
                  [true, 1]
              );
          })
          .catch(fail)
          .then(done);
      });
  });
  