diff --git a/src/traces/scatter/arrays_to_calcdata.js b/src/traces/scatter/arrays_to_calcdata.js index 7cfcf57d2a3..8b81c42bcb7 100644 --- a/src/traces/scatter/arrays_to_calcdata.js +++ b/src/traces/scatter/arrays_to_calcdata.js @@ -16,6 +16,7 @@ var Lib = require('../../lib'); module.exports = function arraysToCalcdata(cd, trace) { Lib.mergeArray(trace.text, cd, 'tx'); + Lib.mergeArray(trace.customdata, cd, 'data'); Lib.mergeArray(trace.textposition, cd, 'tp'); if(trace.textfont) { Lib.mergeArray(trace.textfont.size, cd, 'ts'); diff --git a/src/traces/scatter/attributes.js b/src/traces/scatter/attributes.js index 0354006573d..fc1412e9f05 100644 --- a/src/traces/scatter/attributes.js +++ b/src/traces/scatter/attributes.js @@ -56,6 +56,10 @@ module.exports = { 'where `y0` is the starting coordinate and `dy` the step.' ].join(' ') }, + customdata: { + valType: 'data_array', + description: 'Assigns extra data to each scatter point DOM element' + }, dy: { valType: 'number', dflt: 1, diff --git a/src/traces/scatter/defaults.js b/src/traces/scatter/defaults.js index 6b71dbd4f6f..c0115bb2ab7 100644 --- a/src/traces/scatter/defaults.js +++ b/src/traces/scatter/defaults.js @@ -36,6 +36,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout return; } + coerce('customdata'); coerce('text'); coerce('mode', defaultMode); coerce('ids'); diff --git a/src/traces/scatter/plot.js b/src/traces/scatter/plot.js index b9dde96cd7a..7da03ffe2fe 100644 --- a/src/traces/scatter/plot.js +++ b/src/traces/scatter/plot.js @@ -428,9 +428,14 @@ function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, transition } join.each(function(d) { - var sel = transition(d3.select(this)); + var el = d3.select(this); + var sel = transition(el); Drawing.translatePoint(d, sel, xa, ya); Drawing.singlePointStyle(d, sel, trace); + + if(trace.customdata) { + el.classed('plotly-customdata', d.data !== null && d.data !== undefined); + } }); if(hasTransition) { diff --git a/src/traces/scatter/style.js b/src/traces/scatter/style.js index 78339d3acf7..9f0c17a935d 100644 --- a/src/traces/scatter/style.js +++ b/src/traces/scatter/style.js @@ -24,10 +24,14 @@ module.exports = function style(gd) { s.selectAll('g.points') .each(function(d) { - d3.select(this).selectAll('path.point') - .call(Drawing.pointStyle, d.trace || d[0].trace); - d3.select(this).selectAll('text') - .call(Drawing.textPointStyle, d.trace || d[0].trace); + var el = d3.select(this); + var pts = el.selectAll('path.point'); + var trace = d.trace || d[0].trace; + + pts.call(Drawing.pointStyle, trace); + + el.selectAll('text') + .call(Drawing.textPointStyle, trace); }); s.selectAll('g.trace path.js-line') diff --git a/test/jasmine/tests/calcdata_test.js b/test/jasmine/tests/calcdata_test.js index 9ac9dda0e98..d34664bfd1a 100644 --- a/test/jasmine/tests/calcdata_test.js +++ b/test/jasmine/tests/calcdata_test.js @@ -862,4 +862,18 @@ describe('calculated data and points', function() { }); }); }); + + describe('customdata', function() { + it('should pass customdata to the calcdata points', function() { + Plotly.plot(gd, [{ + x: [0, 1, 3], + y: [4, 5, 7], + customdata: ['a', 'b', {foo: 'bar'}] + }], {}); + + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({data: 'a'})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({data: 'b'})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({data: {foo: 'bar'}})); + }); + }); }); diff --git a/test/jasmine/tests/scatter_test.js b/test/jasmine/tests/scatter_test.js index 1dcb3abbee3..210c418341e 100644 --- a/test/jasmine/tests/scatter_test.js +++ b/test/jasmine/tests/scatter_test.js @@ -1,8 +1,14 @@ +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 fail = require('../assets/fail_test'); + describe('Test scatter', function() { 'use strict'; @@ -326,3 +332,43 @@ describe('Test scatter', function() { }); }); + +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); + }); +});