diff --git a/src/plots/cartesian/axis_autotype.js b/src/plots/cartesian/axis_autotype.js new file mode 100644 index 00000000000..8202a7778d3 --- /dev/null +++ b/src/plots/cartesian/axis_autotype.js @@ -0,0 +1,73 @@ +/** +* Copyright 2012-2016, 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 isNumeric = require('fast-isnumeric'); + +var Lib = require('../../lib'); +var cleanDatum = require('./clean_datum'); + +module.exports = function autoType(array) { + if(moreDates(array)) return 'date'; + if(category(array)) return 'category'; + if(linearOK(array)) return 'linear'; + else return '-'; +}; + +// is there at least one number in array? If not, we should leave +// ax.type empty so it can be autoset later +function linearOK(array) { + if(!array) return false; + + for(var i = 0; i < array.length; i++) { + if(isNumeric(array[i])) return true; + } + + return false; +} + +// does the array a have mostly dates rather than numbers? +// note: some values can be neither (such as blanks, text) +// 2- or 4-digit integers can be both, so require twice as many +// dates as non-dates, to exclude cases with mostly 2 & 4 digit +// numbers and a few dates +function moreDates(a) { + var dcnt = 0, + ncnt = 0, + // test at most 1000 points, evenly spaced + inc = Math.max(1, (a.length - 1) / 1000), + ai; + + for(var i = 0; i < a.length; i += inc) { + ai = a[Math.round(i)]; + if(Lib.isDateTime(ai)) dcnt += 1; + if(isNumeric(ai)) ncnt += 1; + } + + return (dcnt > ncnt * 2); +} + +// are the (x,y)-values in td.data mostly text? +// require twice as many categories as numbers +function category(a) { + // test at most 1000 points + var inc = Math.max(1, (a.length - 1) / 1000), + curvenums = 0, + curvecats = 0, + ai; + + for(var i = 0; i < a.length; i += inc) { + ai = cleanDatum(a[Math.round(i)]); + if(isNumeric(ai)) curvenums++; + else if(typeof ai === 'string' && ai !== '' && ai !== 'None') curvecats++; + } + + return curvecats > curvenums * 2; +} diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js index 015ec9d7cd0..7d27bffc437 100644 --- a/src/plots/cartesian/axis_defaults.js +++ b/src/plots/cartesian/axis_defaults.js @@ -23,8 +23,8 @@ var handleTickLabelDefaults = require('./tick_label_defaults'); var handleCategoryOrderDefaults = require('./category_order_defaults'); var setConvert = require('./set_convert'); var orderedCategories = require('./ordered_categories'); -var cleanDatum = require('./clean_datum'); var axisIds = require('./axis_ids'); +var autoType = require('./axis_autotype'); /** @@ -207,13 +207,6 @@ function isBoxWithoutPositionCoords(trace, axLetter) { ); } -function autoType(array) { - if(moreDates(array)) return 'date'; - if(category(array)) return 'category'; - if(linearOK(array)) return 'linear'; - else return '-'; -} - function getFirstNonEmptyTrace(data, id, axLetter) { for(var i = 0; i < data.length; i++) { var trace = data[i]; @@ -228,54 +221,3 @@ function getFirstNonEmptyTrace(data, id, axLetter) { } } } - -// is there at least one number in array? If not, we should leave -// ax.type empty so it can be autoset later -function linearOK(array) { - if(!array) return false; - - for(var i = 0; i < array.length; i++) { - if(isNumeric(array[i])) return true; - } - - return false; -} - -// does the array a have mostly dates rather than numbers? -// note: some values can be neither (such as blanks, text) -// 2- or 4-digit integers can be both, so require twice as many -// dates as non-dates, to exclude cases with mostly 2 & 4 digit -// numbers and a few dates -function moreDates(a) { - var dcnt = 0, - ncnt = 0, - // test at most 1000 points, evenly spaced - inc = Math.max(1, (a.length - 1) / 1000), - ai; - - for(var i = 0; i < a.length; i += inc) { - ai = a[Math.round(i)]; - if(Lib.isDateTime(ai)) dcnt += 1; - if(isNumeric(ai)) ncnt += 1; - } - - return (dcnt > ncnt * 2); -} - -// are the (x,y)-values in td.data mostly text? -// require twice as many categories as numbers -function category(a) { - // test at most 1000 points - var inc = Math.max(1, (a.length - 1) / 1000), - curvenums = 0, - curvecats = 0, - ai; - - for(var i = 0; i < a.length; i += inc) { - ai = cleanDatum(a[Math.round(i)]); - if(isNumeric(ai)) curvenums++; - else if(typeof ai === 'string' && ai !== '' && ai !== 'None') curvecats++; - } - - return curvecats > curvenums * 2; -} diff --git a/src/traces/scattergl/convert.js b/src/traces/scattergl/convert.js index fe7a44ebaf3..339d32679d2 100644 --- a/src/traces/scattergl/convert.js +++ b/src/traces/scattergl/convert.js @@ -17,6 +17,7 @@ var isNumeric = require('fast-isnumeric'); var Lib = require('../../lib'); var Axes = require('../../plots/cartesian/axes'); +var autoType = require('../../plots/cartesian/axis_autotype'); var ErrorBars = require('../../components/errorbars'); var str2RGBArray = require('../../lib/str2rgbarray'); var truncate = require('../../lib/float32_truncate'); @@ -114,11 +115,13 @@ proto.handlePick = function(pickResult) { index = this.idToIndex[pickResult.pointId]; } + var x = this.pickXData[index]; + return { trace: this, dataCoord: pickResult.dataCoord, traceCoord: [ - this.pickXData[index], + isNumeric(x) || !Lib.isDateTime(x) ? x : Lib.dateTime2ms(x), this.pickYData[index] ], textLabel: Array.isArray(this.textLabels) ? @@ -135,7 +138,7 @@ proto.handlePick = function(pickResult) { // check if trace is fancy proto.isFancy = function(options) { - if(this.scene.xaxis.type !== 'linear') return true; + if(this.scene.xaxis.type !== 'linear' && this.scene.xaxis.type !== 'date') return true; if(this.scene.yaxis.type !== 'linear') return true; if(!options.x || !options.y) return true; @@ -259,6 +262,29 @@ proto.update = function(options) { this.color = getTraceColor(options, {}); }; +// We'd ideally know that all values are of fast types; sampling gives no certainty but faster +// (for the future, typed arrays can guarantee it, and Date values can be done with +// representing the epoch milliseconds in a typed array; +// also, perhaps the Python / R interfaces take care of String->Date conversions +// such that there's no need to check for string dates in plotly.js) +// Patterned from axis_defaults.js:moreDates +// Code DRYing is not done to preserve the most direct compilation possible for speed; +// also, there are quite a few differences +function allFastTypesLikely(a) { + var len = a.length, + inc = Math.max(0, (len - 1) / Math.min(Math.max(len, 1), 1000)), + ai; + + for(var i = 0; i < len; i += inc) { + ai = a[Math.floor(i)]; + if(!isNumeric(ai) && !(ai instanceof Date)) { + return false; + } + } + + return true; +} + proto.updateFast = function(options) { var x = this.xData = this.pickXData = options.x; var y = this.yData = this.pickYData = options.y; @@ -272,24 +298,34 @@ proto.updateFast = function(options) { var xx, yy; + var fastType = allFastTypesLikely(x); + var isDateTime = !fastType && autoType(x) === 'date'; + // TODO add 'very fast' mode that bypasses this loop // TODO bypass this on modebar +/- zoom - for(var i = 0; i < len; ++i) { - xx = x[i]; - yy = y[i]; + if(fastType || isDateTime) { - // check for isNaN is faster but doesn't skip over nulls - if(!isNumeric(xx) || !isNumeric(yy)) continue; + for(var i = 0; i < len; ++i) { + xx = x[i]; + yy = y[i]; - idToIndex[pId++] = i; + if(isNumeric(yy)) { - positions[ptr++] = xx; - positions[ptr++] = yy; + if(!fastType) { + xx = Lib.dateTime2ms(xx); + } + + idToIndex[pId++] = i; - bounds[0] = Math.min(bounds[0], xx); - bounds[1] = Math.min(bounds[1], yy); - bounds[2] = Math.max(bounds[2], xx); - bounds[3] = Math.max(bounds[3], yy); + positions[ptr++] = xx; + positions[ptr++] = yy; + + bounds[0] = Math.min(bounds[0], xx); + bounds[1] = Math.min(bounds[1], yy); + bounds[2] = Math.max(bounds[2], xx); + bounds[3] = Math.max(bounds[3], yy); + } + } } positions = truncate(positions, ptr); diff --git a/test/jasmine/tests/gl2d_date_axis_render_test.js b/test/jasmine/tests/gl2d_date_axis_render_test.js new file mode 100644 index 00000000000..53392de3e76 --- /dev/null +++ b/test/jasmine/tests/gl2d_date_axis_render_test.js @@ -0,0 +1,99 @@ +var PlotlyInternal = require('@src/plotly'); + +var hasWebGLSupport = require('../assets/has_webgl_support'); + +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); + +describe('date axis', function() { + + if(!hasWebGLSupport('axes_test date axis')) return; + + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(destroyGraphDiv); + + it('should use the fancy gl-vis/gl-scatter2d', function() { + PlotlyInternal.plot(gd, [{ + type: 'scattergl', + 'marker': { + 'color': 'rgb(31, 119, 180)', + 'size': 18, + 'symbol': [ + 'diamond', + 'cross' + ] + }, + x: [new Date('2016-10-10'), new Date('2016-10-12')], + y: [15, 16] + }]); + + expect(gd._fullLayout.xaxis.type).toBe('date'); + expect(gd._fullLayout.yaxis.type).toBe('linear'); + expect(gd._fullData[0].type).toBe('scattergl'); + expect(gd._fullData[0]._module.basePlotModule.name).toBe('gl2d'); + + // one way of check which renderer - fancy vs not - we're using + expect(gd._fullLayout._plots.xy._scene2d.glplot.objects[3].pointCount).toBe(0); + }); + + it('should use the fancy gl-vis/gl-scatter2d once again', function() { + PlotlyInternal.plot(gd, [{ + type: 'scattergl', + 'marker': { + 'color': 'rgb(31, 119, 180)', + 'size': 36, + 'symbol': [ + 'circle', + 'cross' + ] + }, + x: [new Date('2016-10-10'), new Date('2016-10-11')], + y: [15, 16] + }]); + + expect(gd._fullLayout.xaxis.type).toBe('date'); + expect(gd._fullLayout.yaxis.type).toBe('linear'); + expect(gd._fullData[0].type).toBe('scattergl'); + expect(gd._fullData[0]._module.basePlotModule.name).toBe('gl2d'); + + // one way of check which renderer - fancy vs not - we're using + expect(gd._fullLayout._plots.xy._scene2d.glplot.objects[3].pointCount).toBe(0); + }); + + it('should now use the non-fancy gl-vis/gl-scatter2d', function() { + PlotlyInternal.plot(gd, [{ + type: 'scattergl', + mode: 'markers', // important, as otherwise lines are assumed (which needs fancy) + x: [new Date('2016-10-10'), new Date('2016-10-11')], + y: [15, 16] + }]); + + expect(gd._fullLayout.xaxis.type).toBe('date'); + expect(gd._fullLayout.yaxis.type).toBe('linear'); + expect(gd._fullData[0].type).toBe('scattergl'); + expect(gd._fullData[0]._module.basePlotModule.name).toBe('gl2d'); + + expect(gd._fullLayout._plots.xy._scene2d.glplot.objects[3].pointCount).toBe(2); + }); + + it('should use the non-fancy gl-vis/gl-scatter2d with string dates', function() { + PlotlyInternal.plot(gd, [{ + type: 'scattergl', + mode: 'markers', // important, as otherwise lines are assumed (which needs fancy) + x: ['2016-10-10', '2016-10-11'], + y: [15, 16] + }]); + + expect(gd._fullLayout.xaxis.type).toBe('date'); + expect(gd._fullLayout.yaxis.type).toBe('linear'); + expect(gd._fullData[0].type).toBe('scattergl'); + expect(gd._fullData[0]._module.basePlotModule.name).toBe('gl2d'); + + expect(gd._fullLayout._plots.xy._scene2d.glplot.objects[3].pointCount).toBe(2); + }); +});