Skip to content

Non-fancy scattergl to work with dates #1021

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Oct 24, 2016
73 changes: 73 additions & 0 deletions src/plots/cartesian/axis_autotype.js
Original file line number Diff line number Diff line change
@@ -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;
}
60 changes: 1 addition & 59 deletions src/plots/cartesian/axis_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');


/**
Expand Down Expand Up @@ -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];
Expand All @@ -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;
}
64 changes: 50 additions & 14 deletions src/traces/scattergl/convert.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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) ?
Expand All @@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

if(this.scene.yaxis.type !== 'linear') return true;

if(!options.x || !options.y) return true;
Expand Down Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the comment.

such that there's no need to check for string dates in plotly.js

That won't happen any time soon unfortunately ...

// 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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very nicely done.

}

proto.updateFast = function(options) {
var x = this.xData = this.pickXData = options.x;
var y = this.yData = this.pickYData = options.y;
Expand All @@ -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);
Expand Down
99 changes: 99 additions & 0 deletions test/jasmine/tests/gl2d_date_axis_render_test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});