Skip to content

Commit 0aa0f5e

Browse files
committed
improvements to Scattergl.calc
- reuse scatter axis-expansion logic - improve 'fast' axis expand routine (using average marker.size as pad value) - use ax.makeCalcdata for all axis types (this creates a new array for linear axes, but makes thing more robust) - add a few TODOs
1 parent 5316c47 commit 0aa0f5e

File tree

3 files changed

+124
-133
lines changed

3 files changed

+124
-133
lines changed

src/plots/cartesian/axes.js

+8-6
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,13 @@ axes.saveShowSpikeInitial = function(gd, overwrite) {
423423
return hasOneAxisChanged;
424424
};
425425

426+
axes.doesAxisNeedAutoRange = function(ax) {
427+
return (
428+
ax.autorange ||
429+
!!Lib.nestedProperty(ax, 'rangeslider.autorange').get()
430+
);
431+
};
432+
426433
// axes.expand: if autoranging, include new data in the outer limits
427434
// for this axis
428435
// data is an array of numbers (ie already run through ax.d2c)
@@ -436,12 +443,7 @@ axes.saveShowSpikeInitial = function(gd, overwrite) {
436443
// tozero: (boolean) make sure to include zero if axis is linear,
437444
// and make it a tight bound if possible
438445
axes.expand = function(ax, data, options) {
439-
var needsAutorange = (
440-
ax.autorange ||
441-
!!Lib.nestedProperty(ax, 'rangeslider.autorange').get()
442-
);
443-
444-
if(!needsAutorange || !data) return;
446+
if(!axes.doesAxisNeedAutoRange(ax) || !data) return;
445447

446448
if(!ax._min) ax._min = [];
447449
if(!ax._max) ax._max = [];

src/traces/scatter/calc.js

+30-21
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,31 @@ function calc(gd, trace) {
2525
var x = xa.makeCalcdata(trace, 'x');
2626
var y = ya.makeCalcdata(trace, 'y');
2727
var serieslen = trace._length;
28+
var cd = new Array(serieslen);
29+
30+
var ppad = calcMarkerSize(trace, serieslen);
31+
calcAxisExpansion(gd, trace, xa, ya, x, y, ppad);
32+
33+
for(var i = 0; i < serieslen; i++) {
34+
cd[i] = (isNumeric(x[i]) && isNumeric(y[i])) ?
35+
{x: x[i], y: y[i]} :
36+
{x: BADNUM, y: BADNUM};
37+
38+
if(trace.ids) {
39+
cd[i].id = String(trace.ids[i]);
40+
}
41+
}
42+
43+
arraysToCalcdata(cd, trace);
44+
calcColorscale(trace);
45+
calcSelection(cd, trace);
46+
47+
gd.firstscatter = false;
48+
return cd;
49+
}
50+
51+
function calcAxisExpansion(gd, trace, xa, ya, x, y, ppad) {
52+
var serieslen = trace._length;
2853

2954
// cancel minimum tick spacings (only applies to bars and boxes)
3055
xa._minDtick = 0;
@@ -35,8 +60,9 @@ function calc(gd, trace) {
3560
var xOptions = {padded: true};
3661
var yOptions = {padded: true};
3762

38-
var ppad = calcMarkerSize(trace, serieslen);
39-
if(ppad) xOptions.ppad = yOptions.ppad = ppad;
63+
if(ppad) {
64+
xOptions.ppad = yOptions.ppad = ppad;
65+
}
4066

4167
// TODO: text size
4268

@@ -72,24 +98,6 @@ function calc(gd, trace) {
7298

7399
Axes.expand(xa, x, xOptions);
74100
Axes.expand(ya, y, yOptions);
75-
76-
// create the "calculated data" to plot
77-
var cd = new Array(serieslen);
78-
for(var i = 0; i < serieslen; i++) {
79-
cd[i] = (isNumeric(x[i]) && isNumeric(y[i])) ?
80-
{x: x[i], y: y[i]} : {x: BADNUM, y: BADNUM};
81-
82-
if(trace.ids) {
83-
cd[i].id = String(trace.ids[i]);
84-
}
85-
}
86-
87-
arraysToCalcdata(cd, trace);
88-
calcColorscale(trace);
89-
calcSelection(cd, trace);
90-
91-
gd.firstscatter = false;
92-
return cd;
93101
}
94102

95103
function calcMarkerSize(trace, serieslen) {
@@ -131,5 +139,6 @@ function calcMarkerSize(trace, serieslen) {
131139

132140
module.exports = {
133141
calc: calc,
134-
calcMarkerSize: calcMarkerSize
142+
calcMarkerSize: calcMarkerSize,
143+
calcAxisExpansion: calcAxisExpansion
135144
};

src/traces/scattergl/index.js

+86-106
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,15 @@ var formatColor = require('../../lib/gl_format_color');
2525

2626
var subTypes = require('../scatter/subtypes');
2727
var calcMarkerSize = require('../scatter/calc').calcMarkerSize;
28+
var calcAxisExpansion = require('../scatter/calc').calcAxisExpansion;
2829
var calcColorscales = require('../scatter/colorscale_calc');
2930
var makeBubbleSizeFn = require('../scatter/make_bubble_size_func');
3031
var linkTraces = require('../scatter/link_traces');
3132
var getTraceColor = require('../scatter/get_trace_color');
3233
var fillHoverText = require('../scatter/fill_hover_text');
33-
var isNumeric = require('fast-isnumeric');
3434

3535
var DASHES = require('../../constants/gl2d_dashes');
36+
var BADNUM = require('../../constants/numerical').BADNUM;
3637
var SYMBOL_SDF_SIZE = 200;
3738
var SYMBOL_SIZE = 20;
3839
var SYMBOL_STROKE = SYMBOL_SIZE / 20;
@@ -47,116 +48,73 @@ function calc(gd, trace) {
4748
var xa = Axes.getFromId(gd, trace.xaxis);
4849
var ya = Axes.getFromId(gd, trace.yaxis);
4950
var subplot = fullLayout._plots[trace.xaxis + trace.yaxis];
51+
var count = trace._length;
52+
var count2 = count * 2;
5053
var stash = {};
54+
var i, xx, yy;
5155

56+
var x = xa.makeCalcdata(trace, 'x');
57+
var y = ya.makeCalcdata(trace, 'y');
5258

53-
var x = xaxis.type === 'linear' ? trace.x : xaxis.makeCalcdata(trace, 'x');
54-
var y = yaxis.type === 'linear' ? trace.y : yaxis.makeCalcdata(trace, 'y');
55-
56-
var count = trace._length, i, xx, yy;
59+
// we need hi-precision for scatter2d,
60+
// regl-scatter2d uses NaNs for bad/missing values
61+
//
62+
// TODO should this be a Float32Array ??
63+
var positions = new Array(count2);
64+
for(i = 0; i < count; i++) {
65+
xx = x[i];
66+
yy = y[i];
67+
// TODO does d2c output any other bad value as BADNUM ever?
68+
positions[i * 2] = xx === BADNUM ? NaN : xx;
69+
positions[i * 2 + 1] = yy === BADNUM ? NaN : yy;
70+
}
5771

58-
if(!x) {
59-
x = Array(count);
60-
for(i = 0; i < count; i++) {
61-
x[i] = i;
72+
if(xa.type === 'log') {
73+
for(i = 0; i < count2; i += 2) {
74+
positions[i] = xa.d2l(positions[i]);
6275
}
6376
}
64-
if(!y) {
65-
y = Array(count);
66-
for(i = 0; i < count; i++) {
67-
y[i] = i;
77+
if(ya.type === 'log') {
78+
for(i = 1; i < count2; i += 2) {
79+
positions[i] = ya.d2l(positions[i]);
6880
}
6981
}
7082

71-
// get log converted positions
72-
var rawx = (xaxis.type === 'log' || x.length > count) ? x.slice(0, count) : x;
73-
var rawy = (yaxis.type === 'log' || y.length > count) ? y.slice(0, count) : y;
74-
75-
var convertX = (xaxis.type === 'log') ? xaxis.d2l : parseFloat;
76-
var convertY = (yaxis.type === 'log') ? yaxis.d2l : parseFloat;
77-
78-
// we need hi-precision for scatter2d
79-
positions = new Array(count * 2);
80-
81-
for(i = 0; i < count; i++) {
82-
x[i] = convertX(x[i]);
83-
y[i] = convertY(y[i]);
84-
85-
// if no x defined, we are creating simple int sequence (API)
86-
// we use parseFloat because it gives NaN (we need that for empty values to avoid drawing lines) and it is incredibly fast
87-
xx = isNumeric(x[i]) ? +x[i] : NaN;
88-
yy = isNumeric(y[i]) ? +y[i] : NaN;
89-
90-
positions[i * 2] = xx;
91-
positions[i * 2 + 1] = yy;
92-
}
93-
9483
// we don't build a tree for log axes since it takes long to convert log2px
9584
// and it is also
96-
if(xaxis.type !== 'log' && yaxis.type !== 'log') {
85+
if(xa.type !== 'log' && ya.type !== 'log') {
9786
// FIXME: delegate this to webworker
9887
stash.tree = kdtree(positions, 512);
99-
}
100-
else {
101-
var ids = stash.ids = Array(count);
88+
} else {
89+
var ids = stash.ids = new Array(count);
10290
for(i = 0; i < count; i++) {
10391
ids[i] = i;
10492
}
10593
}
10694

95+
// create scene options and scene
10796
calcColorscales(trace);
108-
109-
var options = sceneOptions(container, subplot, trace, positions);
110-
111-
// expanding axes is separate from options
112-
if(!options.markers) {
113-
Axes.expand(xaxis, rawx, { padded: true });
114-
Axes.expand(yaxis, rawy, { padded: true });
115-
}
116-
else if(Lib.isArrayOrTypedArray(options.markers.sizes)) {
117-
var sizes = options.markers.sizes;
118-
Axes.expand(xaxis, rawx, { padded: true, ppad: sizes });
119-
Axes.expand(yaxis, rawy, { padded: true, ppad: sizes });
120-
}
121-
else {
122-
var xbounds = [Infinity, -Infinity], ybounds = [Infinity, -Infinity];
123-
var size = options.markers.size;
124-
125-
// axes bounds
126-
for(i = 0; i < count; i++) {
127-
xx = x[i], yy = y[i];
128-
if(xbounds[0] > xx) xbounds[0] = xx;
129-
if(xbounds[1] < xx) xbounds[1] = xx;
130-
if(ybounds[0] > yy) ybounds[0] = yy;
131-
if(ybounds[1] < yy) ybounds[1] = yy;
132-
}
133-
134-
// FIXME: is there a better way to separate expansion?
135-
if(count < TOO_MANY_POINTS) {
136-
Axes.expand(xaxis, rawx, { padded: true, ppad: size });
137-
Axes.expand(yaxis, rawy, { padded: true, ppad: size });
138-
}
139-
// update axes fast for big number of points
140-
else {
141-
if(xaxis._min) {
142-
xaxis._min.push({ val: xbounds[0], pad: size });
143-
}
144-
if(xaxis._max) {
145-
xaxis._max.push({ val: xbounds[1], pad: size });
146-
}
147-
148-
if(yaxis._min) {
149-
yaxis._min.push({ val: ybounds[0], pad: size });
150-
}
151-
if(yaxis._max) {
152-
yaxis._max.push({ val: ybounds[1], pad: size });
153-
}
97+
var options = sceneOptions(gd, subplot, trace, positions);
98+
var markerOptions = options.marker;
99+
var scene = sceneUpdate(gd, subplot);
100+
var ppad;
101+
102+
// Re-use SVG scatter axis expansion routine except
103+
// for graph with very large number of points where it
104+
// performs poorly.
105+
// In big data case, fake Axes.expand outputs with data bounds,
106+
// and an average size for array marker.size inputs.
107+
if(count < TOO_MANY_POINTS) {
108+
ppad = calcMarkerSize(trace, count);
109+
calcAxisExpansion(gd, trace, xa, ya, x, y, ppad);
110+
} else {
111+
if(markerOptions) {
112+
ppad = 2 * (markerOptions.sizeAvg || Math.max(markerOptions.size, 3));
154113
}
114+
fastAxisExpand(xa, x, ppad);
115+
fastAxisExpand(ya, y, ppad);
155116
}
156117

157-
// create scene
158-
var scene = sceneUpdate(container, subplot);
159-
160118
// set flags to create scene renderers
161119
if(options.fill && !scene.fill2d) scene.fill2d = true;
162120
if(options.marker && !scene.scatter2d) scene.scatter2d = true;
@@ -178,14 +136,33 @@ function calc(gd, trace) {
178136
stash.index = scene.count - 1;
179137
stash.x = x;
180138
stash.y = y;
181-
stash.rawx = rawx;
182-
stash.rawy = rawy;
183139
stash.positions = positions;
184140
stash.count = count;
185141

142+
gd.firstscatter = false;
186143
return [{x: false, y: false, t: stash, trace: trace}];
187144
}
188145

146+
// Approximate Axes.expand results with speed
147+
function fastAxisExpand(ax, vals, ppad) {
148+
if(!Axes.doesAxisNeedAutoRange(ax) || !vals) return;
149+
150+
var b0 = Infinity;
151+
var b1 = -Infinity;
152+
153+
for(var i = 0; i < vals.length; i += 2) {
154+
var v = vals[i];
155+
if(v < b0) b0 = v;
156+
if(v > b1) b1 = v;
157+
}
158+
159+
if(ax._min) ax._min = [];
160+
ax._min.push({val: b0, pad: ppad});
161+
162+
if(ax._max) ax._max = [];
163+
ax._max.push({val: b1, pad: ppad});
164+
}
165+
189166
// create scene options
190167
function sceneOptions(gd, subplot, trace, positions) {
191168
var fullLayout = gd._fullLayout;
@@ -481,11 +458,15 @@ function sceneOptions(gd, subplot, trace, positions) {
481458
if(multiSize || multiLineWidth) {
482459
var sizes = markerOptions.sizes = new Array(count);
483460
var borderSizes = markerOptions.borderSizes = new Array(count);
461+
var sizeTotal = 0;
462+
var sizeAvg;
484463

485464
if(multiSize) {
486465
for(i = 0; i < count; i++) {
487466
sizes[i] = markerSizeFunc(markerOpts.size[i]);
467+
sizeTotal += sizes[i];
488468
}
469+
sizeAvg = sizeTotal / count;
489470
} else {
490471
s = markerSizeFunc(markerOpts.size);
491472
for(i = 0; i < count; i++) {
@@ -504,6 +485,8 @@ function sceneOptions(gd, subplot, trace, positions) {
504485
borderSizes[i] = s;
505486
}
506487
}
488+
489+
markerOptions.sizeAvg = sizeAvg;
507490
} else {
508491
markerOptions.size = markerSizeFunc(markerOpts && markerOpts.size || 10);
509492
markerOptions.borderSizes = markerSizeFunc(markerOpts.line.width);
@@ -887,8 +870,8 @@ function plot(gd, subplot, cdata) {
887870
var trace = cd.trace;
888871
var stash = cd.t;
889872
var id = stash.index;
890-
var x = stash.rawx,
891-
y = stash.rawy;
873+
var x = stash.x;
874+
var y = stash.y;
892875

893876
var xaxis = subplot.xaxis || Axes.getFromId(gd, trace.xaxis || 'x');
894877
var yaxis = subplot.yaxis || Axes.getFromId(gd, trace.yaxis || 'y');
@@ -998,8 +981,8 @@ function hoverPoints(pointData, xval, yval, hovermode) {
998981
var trace = cd[0].trace;
999982
var xa = pointData.xa;
1000983
var ya = pointData.ya;
1001-
var x = stash.rawx;
1002-
var y = stash.rawy;
984+
var x = stash.x;
985+
var y = stash.y;
1003986
var xpx = xa.c2p(xval);
1004987
var ypx = ya.c2p(yval);
1005988
var maxDistance = pointData.distance;
@@ -1155,15 +1138,12 @@ function hoverPoints(pointData, xval, yval, hovermode) {
11551138
}
11561139

11571140
function selectPoints(searchInfo, polygon) {
1158-
var cd = searchInfo.cd,
1159-
selection = [],
1160-
trace = cd[0].trace,
1161-
stash = cd[0].t,
1162-
x = stash.x,
1163-
y = stash.y,
1164-
rawx = stash.rawx,
1165-
rawy = stash.rawy;
1166-
1141+
var cd = searchInfo.cd;
1142+
var selection = [];
1143+
var trace = cd[0].trace;
1144+
var stash = cd[0].t;
1145+
var x = stash.x;
1146+
var y = stash.y;
11671147
var scene = stash.scene;
11681148

11691149
if(!scene) return selection;
@@ -1183,8 +1163,8 @@ function selectPoints(searchInfo, polygon) {
11831163
els.push(i);
11841164
selection.push({
11851165
pointNumber: i,
1186-
x: rawx ? rawx[i] : x[i],
1187-
y: rawy ? rawy[i] : y[i]
1166+
x: x[i],
1167+
y: y[i]
11881168
});
11891169
}
11901170
else {

0 commit comments

Comments
 (0)