Skip to content

Commit e37eeae

Browse files
authored
Merge pull request #1748 from plotly/better-category-axes-conversions
Improved category axes conversions
2 parents 7a281cc + 0fafe14 commit e37eeae

File tree

12 files changed

+256
-125
lines changed

12 files changed

+256
-125
lines changed

src/components/annotations/annotation_defaults.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,12 @@ module.exports = function handleAnnotationDefaults(annIn, annOut, fullLayout, op
8383

8484
// put the actual click data to bind to into private attributes
8585
// so we don't have to do this little bit of logic on every hover event
86-
annOut._xclick = (xClick === undefined) ? annOut.x : xClick;
87-
annOut._yclick = (yClick === undefined) ? annOut.y : yClick;
86+
annOut._xclick = (xClick === undefined) ?
87+
annOut.x :
88+
Axes.cleanPosition(xClick, gdMock, annOut.xref);
89+
annOut._yclick = (yClick === undefined) ?
90+
annOut.y :
91+
Axes.cleanPosition(yClick, gdMock, annOut.yref);
8892
}
8993

9094
return annOut;

src/components/annotations/click.js

+14-5
Original file line numberDiff line numberDiff line change
@@ -83,18 +83,22 @@ function getToggleSets(gd, hoverData) {
8383
explicitOffSet = [],
8484
hoverLen = (hoverData || []).length;
8585

86-
var i, j, anni, showMode, pointj, toggleType;
86+
var i, j, anni, showMode, pointj, xa, ya, toggleType;
8787

8888
for(i = 0; i < annotations.length; i++) {
8989
anni = annotations[i];
9090
showMode = anni.clicktoshow;
91+
9192
if(showMode) {
9293
for(j = 0; j < hoverLen; j++) {
9394
pointj = hoverData[j];
94-
if(pointj.xaxis._id === anni.xref &&
95-
pointj.yaxis._id === anni.yref &&
96-
pointj.xaxis.d2r(pointj.x) === anni._xclick &&
97-
pointj.yaxis.d2r(pointj.y) === anni._yclick
95+
xa = pointj.xaxis;
96+
ya = pointj.yaxis;
97+
98+
if(xa._id === anni.xref &&
99+
ya._id === anni.yref &&
100+
xa.d2r(pointj.x) === clickData2r(anni._xclick, xa) &&
101+
ya.d2r(pointj.y) === clickData2r(anni._yclick, ya)
98102
) {
99103
// match! toggle this annotation
100104
// regardless of its clicktoshow mode
@@ -121,3 +125,8 @@ function getToggleSets(gd, hoverData) {
121125

122126
return {on: onSet, off: offSet, explicitOff: explicitOffSet};
123127
}
128+
129+
// to handle log axes until v2
130+
function clickData2r(d, ax) {
131+
return ax.type === 'log' ? ax.l2r(d) : ax.d2r(d);
132+
}

src/components/annotations3d/convert.js

-22
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010

1111
var Lib = require('../../lib');
1212
var Axes = require('../../plots/cartesian/axes');
13-
var attributes = require('./attributes');
1413

1514
module.exports = function convert(scene) {
1615
var fullSceneLayout = scene.fullSceneLayout;
@@ -61,25 +60,4 @@ function mockAnnAxes(ann, scene) {
6160
ann._ya.l2p = function() {
6261
return 0.5 * (1 - ann.pdata[1] / ann.pdata[3]) * size.h * (domain.y[1] - domain.y[0]);
6362
};
64-
65-
// or do something more similar to 2d
66-
// where Annotations.supplyLayoutDefaults is called after in Plots.doCalcdata
67-
// if category axes are found.
68-
function coerce(attr, dflt) {
69-
return Lib.coerce(ann, ann, attributes, attr, dflt);
70-
}
71-
72-
function coercePosition(axLetter) {
73-
var axName = axLetter + 'axis';
74-
75-
// mock in such way that getFromId grabs correct 3D axis
76-
var gdMock = { _fullLayout: {} };
77-
gdMock._fullLayout[axName] = fullSceneLayout[axName];
78-
79-
return Axes.coercePosition(ann, gdMock, coerce, axLetter, axLetter, 0.5);
80-
}
81-
82-
coercePosition('x');
83-
coercePosition('y');
84-
coercePosition('z');
8563
}

src/components/annotations3d/defaults.js

+15-5
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
'use strict';
1010

1111
var Lib = require('../../lib');
12+
var Axes = require('../../plots/cartesian/axes');
1213
var handleArrayContainerDefaults = require('../../plots/array_container_defaults');
1314
var handleAnnotationCommonDefaults = require('../annotations/common_defaults');
1415
var attributes = require('./attributes');
@@ -26,16 +27,25 @@ function handleAnnotationDefaults(annIn, annOut, sceneLayout, opts, itemOpts) {
2627
return Lib.coerce(annIn, annOut, attributes, attr, dflt);
2728
}
2829

30+
function coercePosition(axLetter) {
31+
var axName = axLetter + 'axis';
32+
33+
// mock in such way that getFromId grabs correct 3D axis
34+
var gdMock = { _fullLayout: {} };
35+
gdMock._fullLayout[axName] = sceneLayout[axName];
36+
37+
return Axes.coercePosition(annOut, gdMock, coerce, axLetter, axLetter, 0.5);
38+
}
39+
40+
2941
var visible = coerce('visible', !itemOpts.itemIsNotPlainObject);
3042
if(!visible) return annOut;
3143

3244
handleAnnotationCommonDefaults(annIn, annOut, opts.fullLayout, coerce);
3345

34-
// do not use Axes.coercePosition here
35-
// as ax._categories aren't filled in at this stage
36-
coerce('x');
37-
coerce('y');
38-
coerce('z');
46+
coercePosition('x');
47+
coercePosition('y');
48+
coercePosition('z');
3949

4050
// if you have one coordinate you should all three
4151
Lib.noneOrAll(annIn, annOut, ['x', 'y', 'z']);

src/lib/index.js

+12
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010
'use strict';
1111

1212
var d3 = require('d3');
13+
var isNumeric = require('fast-isnumeric');
14+
15+
var numConstants = require('../constants/numerical');
16+
var FP_SAFE = numConstants.FP_SAFE;
17+
var BADNUM = numConstants.BADNUM;
1318

1419
var lib = module.exports = {};
1520

@@ -87,6 +92,13 @@ lib.pushUnique = require('./push_unique');
8792

8893
lib.cleanNumber = require('./clean_number');
8994

95+
lib.ensureNumber = function num(v) {
96+
if(!isNumeric(v)) return BADNUM;
97+
v = Number(v);
98+
if(v < -FP_SAFE || v > FP_SAFE) return BADNUM;
99+
return isNumeric(v) ? Number(v) : BADNUM;
100+
};
101+
90102
lib.noop = require('./noop');
91103
lib.identity = require('./identity');
92104

src/plots/cartesian/axes.js

+14-21
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ var ONEDAY = constants.ONEDAY;
2727
var ONEHOUR = constants.ONEHOUR;
2828
var ONEMIN = constants.ONEMIN;
2929
var ONESEC = constants.ONESEC;
30-
var BADNUM = constants.BADNUM;
3130

3231
var axes = module.exports = {};
3332

@@ -100,33 +99,27 @@ axes.coerceRef = function(containerIn, containerOut, gd, attr, dflt, extraOption
10099
* - for other types: coerce them to numbers
101100
*/
102101
axes.coercePosition = function(containerOut, gd, coerce, axRef, attr, dflt) {
103-
var pos,
104-
newPos;
102+
var cleanPos, pos;
105103

106104
if(axRef === 'paper' || axRef === 'pixel') {
105+
cleanPos = Lib.ensureNumber;
107106
pos = coerce(attr, dflt);
108-
}
109-
else {
107+
} else {
110108
var ax = axes.getFromId(gd, axRef);
111-
112109
dflt = ax.fraction2r(dflt);
113110
pos = coerce(attr, dflt);
114-
115-
if(ax.type === 'category') {
116-
// if position is given as a category name, convert it to a number
117-
if(typeof pos === 'string' && (ax._categories || []).length) {
118-
newPos = ax._categories.indexOf(pos);
119-
containerOut[attr] = (newPos === -1) ? dflt : newPos;
120-
return;
121-
}
122-
}
123-
else if(ax.type === 'date') {
124-
containerOut[attr] = Lib.cleanDate(pos, BADNUM, ax.calendar);
125-
return;
126-
}
111+
cleanPos = ax.cleanPos;
127112
}
128-
// finally make sure we have a number (unless date type already returned a string)
129-
containerOut[attr] = isNumeric(pos) ? Number(pos) : dflt;
113+
114+
containerOut[attr] = cleanPos(pos);
115+
};
116+
117+
axes.cleanPosition = function(pos, gd, axRef) {
118+
var cleanPos = (axRef === 'paper' || axRef === 'pixel') ?
119+
Lib.ensureNumber :
120+
axes.getFromId(gd, axRef).cleanPos;
121+
122+
return cleanPos(pos);
130123
};
131124

132125
axes.getDataToCoordFunc = function(gd, trace, target, targetArray) {

src/plots/cartesian/set_convert.js

+25-20
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ var Lib = require('../../lib');
1616
var cleanNumber = Lib.cleanNumber;
1717
var ms2DateTime = Lib.ms2DateTime;
1818
var dateTime2ms = Lib.dateTime2ms;
19+
var ensureNumber = Lib.ensureNumber;
1920

2021
var numConstants = require('../../constants/numerical');
2122
var FP_SAFE = numConstants.FP_SAFE;
@@ -28,13 +29,6 @@ function fromLog(v) {
2829
return Math.pow(10, v);
2930
}
3031

31-
function num(v) {
32-
if(!isNumeric(v)) return BADNUM;
33-
v = Number(v);
34-
if(v < -FP_SAFE || v > FP_SAFE) return BADNUM;
35-
return isNumeric(v) ? Number(v) : BADNUM;
36-
}
37-
3832
/**
3933
* Define the conversion functions for an axis data is used in 5 ways:
4034
*
@@ -152,7 +146,7 @@ module.exports = function setConvert(ax, fullLayout) {
152146
if(index !== undefined) return index;
153147
}
154148

155-
if(typeof v === 'number') { return v; }
149+
if(isNumeric(v)) return +v;
156150
}
157151

158152
function l2p(v) {
@@ -165,8 +159,8 @@ module.exports = function setConvert(ax, fullLayout) {
165159
function p2l(px) { return (px - ax._b) / ax._m; }
166160

167161
// conversions among c/l/p are fairly simple - do them together for all axis types
168-
ax.c2l = (ax.type === 'log') ? toLog : num;
169-
ax.l2c = (ax.type === 'log') ? fromLog : num;
162+
ax.c2l = (ax.type === 'log') ? toLog : ensureNumber;
163+
ax.l2c = (ax.type === 'log') ? fromLog : ensureNumber;
170164

171165
ax.l2p = l2p;
172166
ax.p2l = p2l;
@@ -182,18 +176,20 @@ module.exports = function setConvert(ax, fullLayout) {
182176
if(['linear', '-'].indexOf(ax.type) !== -1) {
183177
// all are data vals, but d and r need cleaning
184178
ax.d2r = ax.r2d = ax.d2c = ax.r2c = ax.d2l = ax.r2l = cleanNumber;
185-
ax.c2d = ax.c2r = ax.l2d = ax.l2r = num;
179+
ax.c2d = ax.c2r = ax.l2d = ax.l2r = ensureNumber;
186180

187181
ax.d2p = ax.r2p = function(v) { return ax.l2p(cleanNumber(v)); };
188182
ax.p2d = ax.p2r = p2l;
183+
184+
ax.cleanPos = ensureNumber;
189185
}
190186
else if(ax.type === 'log') {
191187
// d and c are data vals, r and l are logged (but d and r need cleaning)
192188
ax.d2r = ax.d2l = function(v, clip) { return toLog(cleanNumber(v), clip); };
193189
ax.r2d = ax.r2c = function(v) { return fromLog(cleanNumber(v)); };
194190

195191
ax.d2c = ax.r2l = cleanNumber;
196-
ax.c2d = ax.l2r = num;
192+
ax.c2d = ax.l2r = ensureNumber;
197193

198194
ax.c2r = toLog;
199195
ax.l2d = fromLog;
@@ -203,6 +199,8 @@ module.exports = function setConvert(ax, fullLayout) {
203199

204200
ax.r2p = function(v) { return ax.l2p(cleanNumber(v)); };
205201
ax.p2r = p2l;
202+
203+
ax.cleanPos = ensureNumber;
206204
}
207205
else if(ax.type === 'date') {
208206
// r and d are date strings, l and c are ms
@@ -222,24 +220,31 @@ module.exports = function setConvert(ax, fullLayout) {
222220

223221
ax.d2p = ax.r2p = function(v, _, calendar) { return ax.l2p(dt2ms(v, 0, calendar)); };
224222
ax.p2d = ax.p2r = function(px, r, calendar) { return ms2dt(p2l(px), r, calendar); };
223+
224+
ax.cleanPos = function(v) { return Lib.cleanDate(v, BADNUM, ax.calendar); };
225225
}
226226
else if(ax.type === 'category') {
227-
// d is categories; r, c, and l are indices
228-
// TODO: should r accept category names too?
229-
// ie r2c and r2l would be getCategoryIndex (and r2p would change)
227+
// d is categories (string)
228+
// c and l are indices (numbers)
229+
// r is categories or numbers
230230

231-
ax.d2r = ax.d2c = ax.d2l = setCategoryIndex;
231+
ax.d2c = ax.d2l = setCategoryIndex;
232232
ax.r2d = ax.c2d = ax.l2d = getCategoryName;
233233

234-
// special d2l variant that won't add categories
235-
ax.d2l_noadd = getCategoryIndex;
234+
ax.d2r = ax.d2l_noadd = getCategoryIndex;
236235

237-
ax.r2l = ax.l2r = ax.r2c = ax.c2r = num;
236+
ax.l2r = ax.r2c = ax.c2r = ensureNumber;
237+
ax.r2l = getCategoryIndex;
238238

239239
ax.d2p = function(v) { return ax.l2p(getCategoryIndex(v)); };
240240
ax.p2d = function(px) { return getCategoryName(p2l(px)); };
241-
ax.r2p = ax.l2p;
241+
ax.r2p = ax.d2p;
242242
ax.p2r = p2l;
243+
244+
ax.cleanPos = function(v) {
245+
if(typeof v === 'string' && v !== '') return v;
246+
return ensureNumber(v);
247+
};
243248
}
244249

245250
// find the range value at the specified (linear) fraction of the axis

src/plots/plots.js

+3-21
Original file line numberDiff line numberDiff line change
@@ -2032,7 +2032,7 @@ plots.doCalcdata = function(gd, traces) {
20322032
}
20332033
}
20342034

2035-
var hasCategoryAxis = initCategories(axList);
2035+
initCategories(axList);
20362036

20372037
var hasCalcTransform = false;
20382038

@@ -2101,25 +2101,11 @@ plots.doCalcdata = function(gd, traces) {
21012101
}
21022102

21032103
Registry.getComponentMethod('fx', 'calc')(gd);
2104-
2105-
// To handle the case of components using category names as coordinates, we
2106-
// need to re-supply defaults for these objects now, after calc has
2107-
// finished populating the category mappings
2108-
// Any component that uses `Axes.coercePosition` falls into this category
2109-
if(hasCategoryAxis) {
2110-
var dataReferencedComponents = ['annotations', 'shapes', 'images'];
2111-
for(i = 0; i < dataReferencedComponents.length; i++) {
2112-
Registry.getComponentMethod(dataReferencedComponents[i], 'supplyLayoutDefaults')(
2113-
gd.layout, fullLayout, fullData);
2114-
}
2115-
}
21162104
};
21172105

2106+
// initialize the category list, if there is one, so we start over
2107+
// to be filled in later by ax.d2c
21182108
function initCategories(axList) {
2119-
var hasCategoryAxis = false;
2120-
2121-
// initialize the category list, if there is one, so we start over
2122-
// to be filled in later by ax.d2c
21232109
for(var i = 0; i < axList.length; i++) {
21242110
axList[i]._categories = axList[i]._initialCategories.slice();
21252111

@@ -2128,11 +2114,7 @@ function initCategories(axList) {
21282114
for(var j = 0; j < axList[i]._categories.length; j++) {
21292115
axList[i]._categoriesMap[axList[i]._categories[j]] = j;
21302116
}
2131-
2132-
if(axList[i].type === 'category') hasCategoryAxis = true;
21332117
}
2134-
2135-
return hasCategoryAxis;
21362118
}
21372119

21382120
plots.rehover = function(gd) {

src/traces/heatmap/calc.js

+8-3
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,15 @@ module.exports = function calc(gd, trace) {
5757
z = binned.z;
5858
}
5959
else {
60-
if(hasColumns(trace)) convertColumnData(trace, xa, ya, 'x', 'y', ['z']);
60+
if(hasColumns(trace)) {
61+
convertColumnData(trace, xa, ya, 'x', 'y', ['z']);
62+
x = trace.x;
63+
y = trace.y;
64+
} else {
65+
x = trace.x ? xa.makeCalcdata(trace, 'x') : [];
66+
y = trace.y ? ya.makeCalcdata(trace, 'y') : [];
67+
}
6168

62-
x = trace.x ? xa.makeCalcdata(trace, 'x') : [];
63-
y = trace.y ? ya.makeCalcdata(trace, 'y') : [];
6469
x0 = trace.x0 || 0;
6570
dx = trace.dx || 1;
6671
y0 = trace.y0 || 0;

0 commit comments

Comments
 (0)