diff --git a/lib/index.js b/lib/index.js
index c16212d23ba..ff568c877ac 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -56,7 +56,8 @@ Plotly.register([
 //
 Plotly.register([
     require('./filter'),
-    require('./groupby')
+    require('./groupby'),
+    require('./sort')
 ]);
 
 // components
diff --git a/lib/sort.js b/lib/sort.js
new file mode 100644
index 00000000000..37550cf94dd
--- /dev/null
+++ b/lib/sort.js
@@ -0,0 +1,11 @@
+/**
+* Copyright 2012-2017, Plotly, Inc.
+* All rights reserved.
+*
+* This source code is licensed under the MIT license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+
+'use strict';
+
+module.exports = require('../src/transforms/sort');
diff --git a/src/lib/index.js b/src/lib/index.js
index 21ac36e6668..10b3226ac77 100644
--- a/src/lib/index.js
+++ b/src/lib/index.js
@@ -344,6 +344,29 @@ lib.mergeArray = function(traceAttr, cd, cdAttr) {
     }
 };
 
+/** Returns target as set by 'target' transform attribute
+ *
+ * @param {object} trace : full trace object
+ * @param {object} transformOpts : transform option object
+ *  - target (string} :
+ *      either an attribute string referencing an array in the trace object, or
+ *      a set array.
+ *
+ * @return {array or false} : the target array (NOT a copy!!) or false if invalid
+ */
+lib.getTargetArray = function(trace, transformOpts) {
+    var target = transformOpts.target;
+
+    if(typeof target === 'string' && target) {
+        var array = lib.nestedProperty(trace, target).get();
+        return Array.isArray(array) ? array : false;
+    } else if(Array.isArray(target)) {
+        return target;
+    }
+
+    return false;
+};
+
 /**
  * modified version of jQuery's extend to strip out private objs and functions,
  * and cut arrays down to first <arraylen> or 1 elements
diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js
index a362ff0e015..2317a4f9ebf 100644
--- a/src/plots/cartesian/axes.js
+++ b/src/plots/cartesian/axes.js
@@ -29,13 +29,13 @@ var ONEMIN = constants.ONEMIN;
 var ONESEC = constants.ONESEC;
 var BADNUM = constants.BADNUM;
 
-
 var axes = module.exports = {};
 
 axes.layoutAttributes = require('./layout_attributes');
 axes.supplyLayoutDefaults = require('./layout_defaults');
 
 axes.setConvert = require('./set_convert');
+var autoType = require('./axis_autotype');
 
 var axisIds = require('./axis_ids');
 axes.id2name = axisIds.id2name;
@@ -45,7 +45,6 @@ axes.listIds = axisIds.listIds;
 axes.getFromId = axisIds.getFromId;
 axes.getFromTrace = axisIds.getFromTrace;
 
-
 /*
  * find the list of possible axes to reference with an xref or yref attribute
  * and coerce it to that list
@@ -130,6 +129,48 @@ axes.coercePosition = function(containerOut, gd, coerce, axRef, attr, dflt) {
     containerOut[attr] = isNumeric(pos) ? Number(pos) : dflt;
 };
 
+axes.getDataToCoordFunc = function(gd, trace, target, targetArray) {
+    var ax;
+
+    // If target points to an axis, use the type we already have for that
+    // axis to find the data type. Otherwise use the values to autotype.
+    var d2cTarget = (target === 'x' || target === 'y' || target === 'z') ?
+        target :
+        targetArray;
+
+    // In the case of an array target, make a mock data array
+    // and call supplyDefaults to the data type and
+    // setup the data-to-calc method.
+    if(Array.isArray(d2cTarget)) {
+        ax = {
+            type: autoType(targetArray),
+            _categories: []
+        };
+        axes.setConvert(ax);
+
+        // build up ax._categories (usually done during ax.makeCalcdata()
+        if(ax.type === 'category') {
+            for(var i = 0; i < targetArray.length; i++) {
+                ax.d2c(targetArray[i]);
+            }
+        }
+    } else {
+        ax = axes.getFromTrace(gd, trace, d2cTarget);
+    }
+
+    // if 'target' has corresponding axis
+    // -> use setConvert method
+    if(ax) return ax.d2c;
+
+    // special case for 'ids'
+    // -> cast to String
+    if(d2cTarget === 'ids') return function(v) { return String(v); };
+
+    // otherwise (e.g. numeric-array of 'marker.color' or 'marker.size')
+    // -> cast to Number
+    return function(v) { return +v; };
+};
+
 // empty out types for all axes containing these traces
 // so we auto-set them again
 axes.clearTypes = function(gd, traces) {
diff --git a/src/transforms/filter.js b/src/transforms/filter.js
index 1b5b0ab5faf..2a2a3182d2a 100644
--- a/src/transforms/filter.js
+++ b/src/transforms/filter.js
@@ -11,9 +11,7 @@
 var Lib = require('../lib');
 var Registry = require('../registry');
 var PlotSchema = require('../plot_api/plot_schema');
-var axisIds = require('../plots/cartesian/axis_ids');
-var autoType = require('../plots/cartesian/axis_autotype');
-var setConvert = require('../plots/cartesian/set_convert');
+var Axes = require('../plots/cartesian/axes');
 
 var COMPARISON_OPS = ['=', '!=', '<', '>=', '>', '<='];
 var INTERVAL_OPS = ['[]', '()', '[)', '(]', '][', ')(', '](', ')['];
@@ -144,12 +142,11 @@ exports.supplyDefaults = function(transformIn) {
 exports.calcTransform = function(gd, trace, opts) {
     if(!opts.enabled) return;
 
-    var target = opts.target,
-        filterArray = getFilterArray(trace, target),
-        len = filterArray.length;
-
-    if(!len) return;
+    var targetArray = Lib.getTargetArray(trace, opts);
+    if(!targetArray) return;
 
+    var target = opts.target;
+    var len = targetArray.length;
     var targetCalendar = opts.targetcalendar;
 
     // even if you provide targetcalendar, if target is a string and there
@@ -159,13 +156,8 @@ exports.calcTransform = function(gd, trace, opts) {
         if(attrTargetCalendar) targetCalendar = attrTargetCalendar;
     }
 
-    // if target points to an axis, use the type we already have for that
-    // axis to find the data type. Otherwise use the values to autotype.
-    var d2cTarget = (target === 'x' || target === 'y' || target === 'z') ?
-        target : filterArray;
-
-    var dataToCoord = getDataToCoordFunc(gd, trace, d2cTarget);
-    var filterFunc = getFilterFunc(opts, dataToCoord, targetCalendar);
+    var d2c = Axes.getDataToCoordFunc(gd, trace, target, targetArray);
+    var filterFunc = getFilterFunc(opts, d2c, targetCalendar);
     var arrayAttrs = PlotSchema.findArrayAttributes(trace);
     var originalArrays = {};
 
@@ -203,60 +195,11 @@ exports.calcTransform = function(gd, trace, opts) {
 
     // loop through filter array, fill trace arrays if passed
     for(var i = 0; i < len; i++) {
-        var passed = filterFunc(filterArray[i]);
+        var passed = filterFunc(targetArray[i]);
         if(passed) forAllAttrs(fillFn, i);
     }
 };
 
-function getFilterArray(trace, target) {
-    if(typeof target === 'string' && target) {
-        var array = Lib.nestedProperty(trace, target).get();
-
-        return Array.isArray(array) ? array : [];
-    }
-    else if(Array.isArray(target)) return target.slice();
-
-    return false;
-}
-
-function getDataToCoordFunc(gd, trace, target) {
-    var ax;
-
-    // In the case of an array target, make a mock data array
-    // and call supplyDefaults to the data type and
-    // setup the data-to-calc method.
-    if(Array.isArray(target)) {
-        ax = {
-            type: autoType(target),
-            _categories: []
-        };
-
-        setConvert(ax);
-
-        if(ax.type === 'category') {
-            // build up ax._categories (usually done during ax.makeCalcdata()
-            for(var i = 0; i < target.length; i++) {
-                ax.d2c(target[i]);
-            }
-        }
-    }
-    else {
-        ax = axisIds.getFromTrace(gd, trace, target);
-    }
-
-    // if 'target' has corresponding axis
-    // -> use setConvert method
-    if(ax) return ax.d2c;
-
-    // special case for 'ids'
-    // -> cast to String
-    if(target === 'ids') return function(v) { return String(v); };
-
-    // otherwise (e.g. numeric-array of 'marker.color' or 'marker.size')
-    // -> cast to Number
-    return function(v) { return +v; };
-}
-
 function getFilterFunc(opts, d2c, targetCalendar) {
     var operation = opts.operation,
         value = opts.value,
diff --git a/src/transforms/sort.js b/src/transforms/sort.js
new file mode 100644
index 00000000000..97fbec17b3e
--- /dev/null
+++ b/src/transforms/sort.js
@@ -0,0 +1,133 @@
+/**
+* Copyright 2012-2017, Plotly, Inc.
+* All rights reserved.
+*
+* This source code is licensed under the MIT license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+
+'use strict';
+
+var Lib = require('../lib');
+var PlotSchema = require('../plot_api/plot_schema');
+var Axes = require('../plots/cartesian/axes');
+
+exports.moduleType = 'transform';
+
+exports.name = 'sort';
+
+exports.attributes = {
+    enabled: {
+        valType: 'boolean',
+        dflt: true,
+        description: [
+            'Determines whether this sort transform is enabled or disabled.'
+        ].join(' ')
+    },
+    target: {
+        valType: 'string',
+        strict: true,
+        noBlank: true,
+        arrayOk: true,
+        dflt: 'x',
+        description: [
+            'Sets the target by which the sort transform is applied.',
+
+            'If a string, *target* is assumed to be a reference to a data array',
+            'in the parent trace object.',
+            'To sort about nested variables, use *.* to access them.',
+            'For example, set `target` to *marker.size* to sort',
+            'about the marker size array.',
+
+            'If an array, *target* is then the data array by which',
+            'the sort transform is applied.'
+        ].join(' ')
+    },
+    order: {
+        valType: 'enumerated',
+        values: ['ascending', 'descending'],
+        dflt: 'ascending',
+        description: [
+            'Sets the sort transform order.'
+        ].join(' ')
+    }
+};
+
+exports.supplyDefaults = function(transformIn) {
+    var transformOut = {};
+
+    function coerce(attr, dflt) {
+        return Lib.coerce(transformIn, transformOut, exports.attributes, attr, dflt);
+    }
+
+    var enabled = coerce('enabled');
+
+    if(enabled) {
+        coerce('target');
+        coerce('order');
+    }
+
+    return transformOut;
+};
+
+exports.calcTransform = function(gd, trace, opts) {
+    if(!opts.enabled) return;
+
+    var targetArray = Lib.getTargetArray(trace, opts);
+    if(!targetArray) return;
+
+    var target = opts.target;
+    var len = targetArray.length;
+    var arrayAttrs = PlotSchema.findArrayAttributes(trace);
+    var d2c = Axes.getDataToCoordFunc(gd, trace, target, targetArray);
+    var indices = getIndices(opts, targetArray, d2c);
+
+    for(var i = 0; i < arrayAttrs.length; i++) {
+        var np = Lib.nestedProperty(trace, arrayAttrs[i]);
+        var arrayOld = np.get();
+        var arrayNew = new Array(len);
+
+        for(var j = 0; j < len; j++) {
+            arrayNew[j] = arrayOld[indices[j]];
+        }
+
+        np.set(arrayNew);
+    }
+};
+
+function getIndices(opts, targetArray, d2c) {
+    var len = targetArray.length;
+    var indices = new Array(len);
+
+    var sortedArray = targetArray
+        .slice()
+        .sort(getSortFunc(opts, d2c));
+
+    for(var i = 0; i < len; i++) {
+        var vTarget = targetArray[i];
+
+        for(var j = 0; j < len; j++) {
+            var vSorted = sortedArray[j];
+
+            if(vTarget === vSorted) {
+                indices[j] = i;
+
+                // clear sortedArray item to get correct
+                // index of duplicate items (if any)
+                sortedArray[j] = null;
+                break;
+            }
+        }
+    }
+
+    return indices;
+}
+
+function getSortFunc(opts, d2c) {
+    switch(opts.order) {
+        case 'ascending':
+            return function(a, b) { return d2c(a) - d2c(b); };
+        case 'descending':
+            return function(a, b) { return d2c(b) - d2c(a); };
+    }
+}
diff --git a/test/jasmine/tests/transform_sort_test.js b/test/jasmine/tests/transform_sort_test.js
new file mode 100644
index 00000000000..6125eeadde3
--- /dev/null
+++ b/test/jasmine/tests/transform_sort_test.js
@@ -0,0 +1,343 @@
+var Plotly = require('@lib/index');
+var Plots = require('@src/plots/plots');
+var Lib = require('@src/lib');
+
+var d3 = require('d3');
+var createGraphDiv = require('../assets/create_graph_div');
+var destroyGraphDiv = require('../assets/destroy_graph_div');
+var fail = require('../assets/fail_test');
+var mouseEvent = require('../assets/mouse_event');
+
+describe('Test sort transform defaults:', function() {
+    function _supply(trace, layout) {
+        layout = layout || {};
+        return Plots.supplyTraceDefaults(trace, 0, layout);
+    }
+
+    it('should coerce all attributes', function() {
+        var out = _supply({
+            x: [1, 2, 3],
+            y: [0, 2, 1],
+            transforms: [{
+                type: 'sort',
+                target: 'marker.size',
+                order: 'descending'
+            }]
+        });
+
+        expect(out.transforms[0].type).toEqual('sort');
+        expect(out.transforms[0].target).toEqual('marker.size');
+        expect(out.transforms[0].order).toEqual('descending');
+        expect(out.transforms[0].enabled).toBe(true);
+    });
+
+    it('should skip unsettable attribute when `enabled: false`', function() {
+        var out = _supply({
+            x: [1, 2, 3],
+            y: [0, 2, 1],
+            transforms: [{
+                type: 'sort',
+                enabled: false,
+                target: 'marker.size',
+                order: 'descending'
+            }]
+        });
+
+        expect(out.transforms[0].type).toEqual('sort');
+        expect(out.transforms[0].target).toBeUndefined();
+        expect(out.transforms[0].order).toBeUndefined();
+        expect(out.transforms[0].enabled).toBe(false);
+    });
+});
+
+describe('Test sort transform calc:', function() {
+    var base = {
+        x: [-2, -1, -2, 0, 1, 3, 1],
+        y: [1, 2, 3, 1, 2, 3, 1],
+        ids: ['n0', 'n1', 'n2', 'z', 'p1', 'p2', 'p3'],
+        marker: {
+            color: [0.1, 0.2, 0.3, 0.1, 0.2, 0.3, 0.4],
+            size: [10, 20, 5, 1, 6, 0, 10]
+        },
+        transforms: [{ type: 'sort' }]
+    };
+
+    function extend(update) {
+        return Lib.extendDeep({}, base, update);
+    }
+
+    function calcDatatoTrace(calcTrace) {
+        return calcTrace[0].trace;
+    }
+
+    function _transform(data, layout) {
+        var gd = {
+            data: data,
+            layout: layout || {}
+        };
+
+        Plots.supplyDefaults(gd);
+        Plots.doCalcdata(gd);
+
+        return gd.calcdata.map(calcDatatoTrace);
+    }
+
+    it('should sort all array attributes (ascending case)', function() {
+        var out = _transform([extend({})]);
+
+        expect(out[0].x).toEqual([-2, -2, -1, 0, 1, 1, 3]);
+        expect(out[0].y).toEqual([1, 3, 2, 1, 2, 1, 3]);
+        expect(out[0].ids).toEqual(['n0', 'n2', 'n1', 'z', 'p1', 'p3', 'p2']);
+        expect(out[0].marker.color).toEqual([0.1, 0.3, 0.2, 0.1, 0.2, 0.4, 0.3]);
+        expect(out[0].marker.size).toEqual([10, 5, 20, 1, 6, 10, 0]);
+    });
+
+    it('should sort all array attributes (descending case)', function() {
+        var out = _transform([extend({
+            transforms: [{
+                order: 'descending'
+            }]
+        })]);
+
+        expect(out[0].x).toEqual([3, 1, 1, 0, -1, -2, -2]);
+        expect(out[0].y).toEqual([3, 2, 1, 1, 2, 1, 3]);
+        expect(out[0].ids).toEqual(['p2', 'p1', 'p3', 'z', 'n1', 'n0', 'n2']);
+        expect(out[0].marker.color).toEqual([0.3, 0.2, 0.4, 0.1, 0.2, 0.1, 0.3]);
+        expect(out[0].marker.size).toEqual([0, 6, 10, 1, 20, 10, 5]);
+    });
+
+    it('should sort via nested targets', function() {
+        var out = _transform([extend({
+            transforms: [{
+                target: 'marker.size',
+                order: 'descending'
+            }]
+        })]);
+
+        expect(out[0].x).toEqual([-1, -2, 1, 1, -2, 0, 3]);
+        expect(out[0].y).toEqual([2, 1, 1, 2, 3, 1, 3]);
+        expect(out[0].ids).toEqual(['n1', 'n0', 'p3', 'p1', 'n2', 'z', 'p2']);
+        expect(out[0].marker.color).toEqual([0.2, 0.1, 0.4, 0.2, 0.3, 0.1, 0.3]);
+        expect(out[0].marker.size).toEqual([20, 10, 10, 6, 5, 1, 0]);
+    });
+
+    it('should sort via dates targets', function() {
+        var out = _transform([{
+            x: ['2015-07-20', '2016-12-02', '2016-09-01', '2016-10-21', '2016-10-20'],
+            y: [0, 1, 2, 3, 4, 5],
+            transforms: [{ type: 'sort' }]
+        }]);
+
+        expect(out[0].x).toEqual([
+            '2015-07-20', '2016-09-01', '2016-10-20', '2016-10-21', '2016-12-02'
+        ]);
+        expect(out[0].y).toEqual([0, 2, 4, 3, 1]);
+    });
+
+    it('should sort via custom targets', function() {
+        var out = _transform([extend({
+            transforms: [{
+                target: [10, 20, 30, 10, 20, 30, 0]
+            }]
+        })]);
+
+        expect(out[0].x).toEqual([1, -2, 0, -1, 1, -2, 3]);
+        expect(out[0].y).toEqual([1, 1, 1, 2, 2, 3, 3]);
+        expect(out[0].ids).toEqual(['p3', 'n0', 'z', 'n1', 'p1', 'n2', 'p2']);
+        expect(out[0].marker.color).toEqual([0.4, 0.1, 0.1, 0.2, 0.2, 0.3, 0.3]);
+        expect(out[0].marker.size).toEqual([10, 10, 1, 20, 6, 5, 0]);
+    });
+
+    it('should truncate transformed arrays to target array length (short target case)', function() {
+        var out = _transform([
+            extend({
+                transforms: [{
+                    order: 'descending',
+                    target: [0, 1]
+                }]
+            }
+        ), extend({
+            text: ['A', 'B'],
+            transforms: [{ target: 'text' }]
+        })]);
+
+        expect(out[0].x).toEqual([-1, -2]);
+        expect(out[0].y).toEqual([2, 1]);
+        expect(out[0].ids).toEqual(['n1', 'n0']);
+        expect(out[0].marker.color).toEqual([0.2, 0.1]);
+        expect(out[0].marker.size).toEqual([20, 10]);
+
+        expect(out[1].x).toEqual([-2, -1]);
+        expect(out[1].y).toEqual([1, 2]);
+        expect(out[1].ids).toEqual(['n0', 'n1']);
+        expect(out[1].marker.color).toEqual([0.1, 0.2]);
+        expect(out[1].marker.size).toEqual([10, 20]);
+    });
+
+    it('should truncate transformed arrays to target array length (long target case)', function() {
+        var out = _transform([
+            extend({
+                transforms: [{
+                    order: 'descending',
+                    target: [0, 1, 2, 0, 1, 2, 3, 0, 1, 2, 3]
+                }]
+            }
+        ), extend({
+            text: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'],
+            transforms: [{ target: 'text' }]
+        })]);
+
+        expect(out[0].x).toEqual([1, undefined, -2, 3, undefined, -1, 1, undefined, -2, 0, undefined]);
+        expect(out[0].y).toEqual([1, undefined, 3, 3, undefined, 2, 2, undefined, 1, 1, undefined]);
+        expect(out[0].ids).toEqual(['p3', undefined, 'n2', 'p2', undefined, 'n1', 'p1', undefined, 'n0', 'z', undefined]);
+        expect(out[0].marker.color).toEqual([0.4, undefined, 0.3, 0.3, undefined, 0.2, 0.2, undefined, 0.1, 0.1, undefined]);
+        expect(out[0].marker.size).toEqual([10, undefined, 5, 0, undefined, 20, 6, undefined, 10, 1, undefined]);
+
+        expect(out[1].x).toEqual([-2, -1, -2, 0, 1, 3, 1, undefined, undefined]);
+        expect(out[1].y).toEqual([1, 2, 3, 1, 2, 3, 1, undefined, undefined]);
+        expect(out[1].ids).toEqual(['n0', 'n1', 'n2', 'z', 'p1', 'p2', 'p3', undefined, undefined]);
+        expect(out[1].marker.color).toEqual([0.1, 0.2, 0.3, 0.1, 0.2, 0.3, 0.4, undefined, undefined]);
+        expect(out[1].marker.size).toEqual([10, 20, 5, 1, 6, 0, 10, undefined, undefined]);
+    });
+});
+
+describe('Test sort transform interactions:', function() {
+    afterEach(destroyGraphDiv);
+
+    function _assertFirst(p) {
+        var parts = d3.select('.point').attr('d').split(',').slice(0, 3).join(',');
+        expect(parts).toEqual(p);
+    }
+
+    it('should respond to restyle calls', function(done) {
+        Plotly.plot(createGraphDiv(), [{
+            x: [-2, -1, -2, 0, 1, 3, 1],
+            y: [1, 2, 3, 1, 2, 3, 1],
+            marker: {
+                size: [10, 20, 5, 1, 6, 0, 10]
+            },
+            transforms: [{
+                type: 'sort',
+                target: 'marker.size',
+            }]
+        }])
+        .then(function(gd) {
+            _assertFirst('M0,0A0,0 0 1');
+
+            return Plotly.restyle(gd, 'transforms[0].order', 'descending');
+        })
+        .then(function(gd) {
+            _assertFirst('M10,0A10,10 0 1');
+
+            return Plotly.restyle(gd, 'transforms[0].enabled', false);
+        })
+        .then(function(gd) {
+            _assertFirst('M5,0A5,5 0 1');
+
+            return Plotly.restyle(gd, 'transforms[0].enabled', true);
+        })
+        .then(function() {
+            _assertFirst('M10,0A10,10 0 1');
+        })
+        .catch(fail)
+        .then(done);
+    });
+
+    it('does not preserve hover/click `pointNumber` value', function(done) {
+        var gd = createGraphDiv();
+
+        function getPxPos(gd, id) {
+            var trace = gd.data[0];
+            var fullLayout = gd._fullLayout;
+            var index = trace.ids.indexOf(id);
+
+            return [
+                fullLayout.xaxis.d2p(trace.x[index]),
+                fullLayout.yaxis.d2p(trace.y[index])
+            ];
+        }
+
+        function hover(gd, id) {
+            return new Promise(function(resolve) {
+                gd.once('plotly_hover', function(eventData) {
+                    resolve(eventData);
+                });
+
+                var pos = getPxPos(gd, id);
+                mouseEvent('mousemove', pos[0], pos[1]);
+            });
+        }
+
+        function click(gd, id) {
+            return new Promise(function(resolve) {
+                gd.once('plotly_click', function(eventData) {
+                    resolve(eventData);
+                });
+
+                var pos = getPxPos(gd, id);
+                mouseEvent('mousemove', pos[0], pos[1]);
+                mouseEvent('mousedown', pos[0], pos[1]);
+                mouseEvent('mouseup', pos[0], pos[1]);
+            });
+        }
+
+        function wait() {
+            return new Promise(function(resolve) {
+                setTimeout(resolve, 60);
+            });
+        }
+
+        function assertPt(eventData, x, y, pointNumber, id) {
+            var pt = eventData.points[0];
+
+            expect(pt.x).toEqual(x, 'x');
+            expect(pt.y).toEqual(y, 'y');
+            expect(pt.pointNumber).toEqual(pointNumber, 'pointNumber');
+            expect(pt.fullData.ids[pt.pointNumber]).toEqual(id, 'id');
+        }
+
+        Plotly.plot(gd, [{
+            mode: 'markers',
+            x: [-2, -1, -2, 0, 1, 3, 1],
+            y: [1, 2, 3, 1, 2, 3, 1],
+            ids: ['A', 'B', 'C', 'D', 'E', 'F', 'G'],
+            marker: {
+                size: [10, 20, 5, 1, 6, 0, 10]
+            },
+            transforms: [{
+                enabled: false,
+                type: 'sort',
+                target: 'marker.size',
+            }]
+        }], {
+            width: 500,
+            height: 500,
+            margin: {l: 0, t: 0, r: 0, b: 0},
+            hovermode: 'closest'
+        })
+        .then(function() { return hover(gd, 'D'); })
+        .then(function(eventData) {
+            assertPt(eventData, 0, 1, 3, 'D');
+        })
+        .then(wait)
+        .then(function() { return click(gd, 'G'); })
+        .then(function(eventData) {
+            assertPt(eventData, 1, 1, 6, 'G');
+        })
+        .then(wait)
+        .then(function() {
+            return Plotly.restyle(gd, 'transforms[0].enabled', true);
+        })
+        .then(function() { return hover(gd, 'D'); })
+        .then(function(eventData) {
+            assertPt(eventData, 0, 1, 1, 'D');
+        })
+        .then(wait)
+        .then(function() { return click(gd, 'G'); })
+        .then(function(eventData) {
+            assertPt(eventData, 1, 1, 5, 'G');
+        })
+        .catch(fail)
+        .then(done);
+    });
+});