diff --git a/src/lib/angles.js b/src/lib/angles.js
index ccfd1c83ffd..b5dd27f28f9 100644
--- a/src/lib/angles.js
+++ b/src/lib/angles.js
@@ -27,3 +27,8 @@ exports.wrap180 = function(deg) {
if(Math.abs(deg) > 180) deg -= Math.round(deg / 360) * 360;
return deg;
};
+
+exports.isFullCircle = function(sector) {
+ var arc = Math.abs(sector[1] - sector[0]);
+ return arc === 360;
+};
diff --git a/src/lib/index.js b/src/lib/index.js
index 9df929ce3e4..abe8e1a8fa8 100644
--- a/src/lib/index.js
+++ b/src/lib/index.js
@@ -87,6 +87,7 @@ lib.deg2rad = anglesModule.deg2rad;
lib.rad2deg = anglesModule.rad2deg;
lib.wrap360 = anglesModule.wrap360;
lib.wrap180 = anglesModule.wrap180;
+lib.isFullCircle = anglesModule.isFullCircle;
var geom2dModule = require('./geometry2d');
lib.segmentsIntersect = geom2dModule.segmentsIntersect;
diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js
index 7fcaf882633..39104b1dbad 100644
--- a/src/plots/cartesian/axes.js
+++ b/src/plots/cartesian/axes.js
@@ -554,7 +554,7 @@ axes.calcTicks = function calcTicks(ax) {
// If same angle over a full circle, the last tick vals is a duplicate.
// TODO must do something similar for angular date axes.
- if(ax._id === 'angular' && Math.abs(rng[1] - rng[0]) === 360) {
+ if(isAngular(ax) && Math.abs(rng[1] - rng[0]) === 360) {
vals.pop();
}
@@ -722,7 +722,7 @@ axes.autoTicks = function(ax, roughDTick) {
ax.tick0 = 0;
ax.dtick = Math.ceil(Math.max(roughDTick, 1));
}
- else if(ax._id === 'angular') {
+ else if(isAngular(ax)) {
ax.tick0 = 0;
base = 1;
ax.dtick = roundDTick(roughDTick, base, roundAngles);
@@ -958,7 +958,7 @@ axes.tickText = function(ax, x, hover) {
if(ax.type === 'date') formatDate(ax, out, hover, extraPrecision);
else if(ax.type === 'log') formatLog(ax, out, hover, extraPrecision, hideexp);
else if(ax.type === 'category') formatCategory(ax, out);
- else if(ax._id === 'angular') formatAngle(ax, out, hover, extraPrecision, hideexp);
+ else if(isAngular(ax)) formatAngle(ax, out, hover, extraPrecision, hideexp);
else formatLinear(ax, out, hover, extraPrecision, hideexp);
// add prefix and suffix
@@ -1646,7 +1646,7 @@ axes.doTicksSingle = function(gd, arg, skipTitle) {
else return 'M' + shift + ',0h' + len;
};
}
- else if(axid === 'angular') {
+ else if(isAngular(ax)) {
sides = ['left', 'right'];
transfn = ax._transfn;
tickpathfn = function(shift, len) {
@@ -1682,7 +1682,7 @@ axes.doTicksSingle = function(gd, arg, skipTitle) {
var valsClipped = vals.filter(clipEnds);
// don't clip angular values
- if(ax._id === 'angular') {
+ if(isAngular(ax)) {
valsClipped = vals;
}
@@ -1751,7 +1751,7 @@ axes.doTicksSingle = function(gd, arg, skipTitle) {
return axside === 'right' ? 'start' : 'end';
};
}
- else if(axid === 'angular') {
+ else if(isAngular(ax)) {
ax._labelShift = labelShift;
ax._labelStandoff = labelStandoff;
ax._pad = pad;
@@ -1798,7 +1798,7 @@ axes.doTicksSingle = function(gd, arg, skipTitle) {
maxFontSize = Math.max(maxFontSize, d.fontSize);
});
- if(axid === 'angular') {
+ if(isAngular(ax)) {
tickLabels.each(function(d) {
d3.select(this).select('text')
.call(svgTextUtils.positionText, labelx(d), labely(d));
@@ -2425,3 +2425,7 @@ function swapAxisAttrs(layout, key, xFullAxes, yFullAxes, dfltTitle) {
np(layout, yFullAxes[i]._name + '.' + key).set(xVal);
}
}
+
+function isAngular(ax) {
+ return ax._id === 'angularaxis';
+}
diff --git a/src/plots/polar/helpers.js b/src/plots/polar/helpers.js
deleted file mode 100644
index 6a85594d1a5..00000000000
--- a/src/plots/polar/helpers.js
+++ /dev/null
@@ -1,61 +0,0 @@
-/**
-* Copyright 2012-2018, 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 Lib = require('../../lib');
-
-exports.setConvertAngular = function setConvertAngular(ax) {
- var dir = {clockwise: -1, counterclockwise: 1}[ax.direction];
- var rot = Lib.deg2rad(ax.rotation);
- var _c2rad;
- var _rad2c;
-
- function getTotalNumberOfCategories() {
- return ax.period ?
- Math.max(ax.period, ax._categories.length) :
- ax._categories.length;
- }
-
- if(ax.type === 'linear') {
- _c2rad = function(v, unit) {
- if(unit === 'degrees') return Lib.deg2rad(v);
- return v;
- };
- _rad2c = function(v, unit) {
- if(unit === 'degrees') return Lib.rad2deg(v);
- return v;
- };
- }
- else if(ax.type === 'category') {
- _c2rad = function(v) {
- var tot = getTotalNumberOfCategories();
- return v * 2 * Math.PI / tot;
- };
- _rad2c = function(v) {
- var tot = getTotalNumberOfCategories();
- return v * tot / Math.PI / 2;
- };
- }
-
- function transformRad(v) { return dir * v + rot; }
- function unTransformRad(v) { return (v - rot) / dir; }
-
- // use the shift 'sector' to get right tick labels for non-default
- // angularaxis 'rotation' and/or 'direction'
- ax.unTransformRad = unTransformRad;
-
- // this version is used on hover
- ax._c2rad = _c2rad;
-
- ax.c2rad = function(v, unit) { return transformRad(_c2rad(v, unit)); };
- ax.rad2c = function(v, unit) { return _rad2c(unTransformRad(v), unit); };
-
- ax.c2deg = function(v, unit) { return Lib.rad2deg(ax.c2rad(v, unit)); };
- ax.deg2c = function(v, unit) { return ax.rad2c(Lib.deg2rad(v), unit); };
-};
diff --git a/src/plots/polar/layout_defaults.js b/src/plots/polar/layout_defaults.js
index 850301deef2..42fcbad3a4d 100644
--- a/src/plots/polar/layout_defaults.js
+++ b/src/plots/polar/layout_defaults.js
@@ -19,10 +19,9 @@ var handleTickLabelDefaults = require('../cartesian/tick_label_defaults');
var handleCategoryOrderDefaults = require('../cartesian/category_order_defaults');
var handleLineGridDefaults = require('../cartesian/line_grid_defaults');
var autoType = require('../cartesian/axis_autotype');
-var setConvert = require('../cartesian/set_convert');
-var setConvertAngular = require('./helpers').setConvertAngular;
var layoutAttributes = require('./layout_attributes');
+var setConvert = require('./set_convert');
var constants = require('./constants');
var axisNames = constants.axisNames;
@@ -66,7 +65,7 @@ function handleDefaults(contIn, contOut, coerce, opts) {
});
var visible = coerceAxis('visible');
- setConvert(axOut, layoutOut);
+ setConvert(axOut, contOut, layoutOut);
var dfltColor;
var dfltFontColor;
@@ -140,8 +139,6 @@ function handleDefaults(contIn, contOut, coerce, opts) {
var direction = coerceAxis('direction');
coerceAxis('rotation', {counterclockwise: 0, clockwise: 90}[direction]);
-
- setConvertAngular(axOut);
break;
}
@@ -201,7 +198,7 @@ function handleAxisTypeDefaults(axIn, axOut, coerce, subplotData, dataAttr) {
}
}
- if(trace) {
+ if(trace && trace[dataAttr]) {
axOut.type = autoType(trace[dataAttr], 'gregorian');
}
diff --git a/src/plots/polar/polar.js b/src/plots/polar/polar.js
index e300807ed1c..500659f55db 100644
--- a/src/plots/polar/polar.js
+++ b/src/plots/polar/polar.js
@@ -16,10 +16,12 @@ var Lib = require('../../lib');
var Color = require('../../components/color');
var Drawing = require('../../components/drawing');
var Plots = require('../plots');
-var Axes = require('../cartesian/axes');
+var setConvertCartesian = require('../cartesian/set_convert');
+var setConvertPolar = require('./set_convert');
var doAutoRange = require('../cartesian/autorange').doAutoRange;
-var dragElement = require('../../components/dragelement');
+var doTicksSingle = require('../cartesian/axes').doTicksSingle;
var dragBox = require('../cartesian/dragbox');
+var dragElement = require('../../components/dragelement');
var Fx = require('../../components/fx');
var Titles = require('../../components/titles');
var prepSelect = require('../cartesian/select').prepSelect;
@@ -28,15 +30,14 @@ var setCursor = require('../../lib/setcursor');
var polygonTester = require('../../lib/polygon').tester;
var MID_SHIFT = require('../../constants/alignment').MID_SHIFT;
+var constants = require('./constants');
var _ = Lib._;
var deg2rad = Lib.deg2rad;
var rad2deg = Lib.rad2deg;
var wrap360 = Lib.wrap360;
var wrap180 = Lib.wrap180;
-
-var setConvertAngular = require('./helpers').setConvertAngular;
-var constants = require('./constants');
+var isFullCircle = Lib.isFullCircle;
function Polar(gd, id) {
this.id = id;
@@ -63,7 +64,7 @@ function Polar(gd, id) {
.attr('class', id);
// unfortunately, we have to keep track of some axis tick settings
- // so that we don't have to call Axes.doTicksSingle with its special redraw flag
+ // so that we don't have to call doTicksSingle with its special redraw flag
this.radialTickLayout = null;
this.angularTickLayout = null;
}
@@ -141,7 +142,7 @@ proto.updateLayers = function(fullLayout, polarLayout) {
break;
case 'angular-grid':
sel.style('fill', 'none');
- sel.append('g').classed('angular', 1);
+ sel.append('g').classed('angularaxis', 1);
break;
case 'radial-line':
sel.append('line').style('fill', 'none');
@@ -155,11 +156,40 @@ proto.updateLayers = function(fullLayout, polarLayout) {
join.order();
};
+/* Polar subplots juggle with 6 'axis objects' (!), these are:
+ *
+ * - polarLayout.radialaxis (aka radialLayout in this file):
+ * - polarLayout.angularaxis (aka angularLayout in this file):
+ * used for data -> calcdata conversions (aka d2c) during the calc step
+ *
+ * - this.radialAxis
+ * extends polarLayout.radialaxis, adds mocked 'domain' and
+ * few other keys in order to reuse Cartesian doAutoRange and doTicksSingle,
+ * used for calcdata -> geometric conversions (aka c2g) during the plot step
+ * + setGeometry setups ax.c2g for given ax.range
+ * + setScale setups ax._m,ax._b for given ax.range
+ *
+ * - this.angularAxis
+ * extends polarLayout.angularaxis, adds mocked 'range' and 'domain' and
+ * a few other keys in order to reuse Cartesian doTicksSingle,
+ * used for calcdata -> geometric conversions (aka c2g) during the plot step
+ * + setGeometry setups ax.c2g given ax.rotation, ax.direction & ax._categories,
+ * and mocks ax.range
+ * + setScale setups ax._m,ax._b with that mocked ax.range
+ *
+ * - this.xaxis
+ * - this.yaxis
+ * setup so that polar traces can reuse plot methods of Cartesian traces
+ * which mostly rely on 2pixel methods (e.g ax.c2p)
+ */
proto.updateLayout = function(fullLayout, polarLayout) {
var _this = this;
var layers = _this.layers;
var gs = fullLayout._size;
+ // axis attributes
+ var radialLayout = polarLayout.radialaxis;
+ var angularLayout = polarLayout.angularaxis;
// layout domains
var xDomain = polarLayout.domain.x;
var yDomain = polarLayout.domain.y;
@@ -210,37 +240,23 @@ proto.updateLayout = function(fullLayout, polarLayout) {
var cxx = _this.cxx = cx - xOffset2;
var cyy = _this.cyy = cy - yOffset2;
- var mockOpts = {
- // to get _boundingBox computation right when showticklabels is false
- anchor: 'free',
- position: 0,
- // dummy truthy value to make Axes.doTicksSingle draw the grid
- _counteraxis: true,
- // don't use automargins routine for labels
- automargin: false
- };
-
- _this.radialAxis = Lib.extendFlat({}, polarLayout.radialaxis, mockOpts, {
+ _this.radialAxis = _this.mockAxis(fullLayout, polarLayout, radialLayout, {
_axislayer: layers['radial-axis'],
_gridlayer: layers['radial-grid'],
// make this an 'x' axis to make positioning (especially rotation) easier
_id: 'x',
- _pos: 0,
// convert to 'x' axis equivalent
side: {
counterclockwise: 'top',
clockwise: 'bottom'
- }[polarLayout.radialaxis.side],
+ }[radialLayout.side],
// spans length 1 radius
domain: [0, radius / gs.w]
});
- _this.angularAxis = Lib.extendFlat({}, polarLayout.angularaxis, mockOpts, {
+ _this.angularAxis = _this.mockAxis(fullLayout, polarLayout, angularLayout, {
_axislayer: layers['angular-axis'],
_gridlayer: layers['angular-grid'],
- // angular axes need *special* logic
- _id: 'angular',
- _pos: 0,
side: 'right',
// to get auto nticks right
domain: [0, Math.PI],
@@ -264,7 +280,7 @@ proto.updateLayout = function(fullLayout, polarLayout) {
range: [sectorBBox[0] * rSpan, sectorBBox[2] * rSpan],
domain: xDomain2
};
- Axes.setConvert(xaxis, fullLayout);
+ setConvertCartesian(xaxis, fullLayout);
xaxis.setScale();
var yaxis = _this.yaxis = {
@@ -273,7 +289,7 @@ proto.updateLayout = function(fullLayout, polarLayout) {
range: [sectorBBox[1] * rSpan, sectorBBox[3] * rSpan],
domain: yDomain2
};
- Axes.setConvert(yaxis, fullLayout);
+ setConvertCartesian(yaxis, fullLayout);
yaxis.setScale();
xaxis.isPtWithinRange = function(d) { return _this.isPtWithinSector(d); };
@@ -297,15 +313,34 @@ proto.updateLayout = function(fullLayout, polarLayout) {
_this.framework.selectAll('.crisp').classed('crisp', 0);
};
+proto.mockAxis = function(fullLayout, polarLayout, axLayout, opts) {
+ var commonOpts = {
+ // to get _boundingBox computation right when showticklabels is false
+ anchor: 'free',
+ position: 0,
+ _pos: 0,
+ // dummy truthy value to make doTicksSingle draw the grid
+ _counteraxis: true,
+ // don't use automargins routine for labels
+ automargin: false
+ };
+
+ var ax = Lib.extendFlat(commonOpts, axLayout, opts);
+ setConvertPolar(ax, polarLayout, fullLayout);
+ return ax;
+};
+
proto.doAutoRange = function(fullLayout, polarLayout) {
+ var gd = this.gd;
+ var radialAxis = this.radialAxis;
var radialLayout = polarLayout.radialaxis;
- var ax = this.radialAxis;
- setScale(ax, radialLayout, fullLayout);
- doAutoRange(this.gd, ax);
+ radialAxis.setScale();
+ doAutoRange(gd, radialAxis);
- radialLayout.range = ax.range.slice();
- radialLayout._input.range = ax.range.slice();
+ var rng = radialAxis.range;
+ radialLayout.range = rng.slice();
+ radialLayout._input.range = rng.slice();
};
proto.updateRadialAxis = function(fullLayout, polarLayout) {
@@ -323,6 +358,8 @@ proto.updateRadialAxis = function(fullLayout, polarLayout) {
_this.fillViewInitialKey('radialaxis.angle', radialLayout.angle);
_this.fillViewInitialKey('radialaxis.range', ax.range.slice());
+ ax.setGeometry();
+
// rotate auto tick labels by 180 if in quadrant II and III to make them
// readable from left-to-right
//
@@ -348,7 +385,8 @@ proto.updateRadialAxis = function(fullLayout, polarLayout) {
_this.radialTickLayout = newTickLayout;
}
- Axes.doTicksSingle(gd, ax, true);
+ ax.setScale();
+ doTicksSingle(gd, ax, true);
// stash 'actual' radial axis angle for drag handlers (in degrees)
var angle = _this.radialAxisAngle = _this.vangles ?
@@ -424,54 +462,35 @@ proto.updateAngularAxis = function(fullLayout, polarLayout) {
var cy = _this.cy;
var angularLayout = polarLayout.angularaxis;
var sector = polarLayout.sector;
- var sectorInRad = sector.map(deg2rad);
var ax = _this.angularAxis;
_this.fillViewInitialKey('angularaxis.rotation', angularLayout.rotation);
- // wrapper around c2rad from setConvertAngular
- // note that linear ranges are always set in degrees for Axes.doTicksSingle
- function c2rad(d) {
- return ax.c2rad(d.x, 'degrees');
- }
+ ax.setGeometry();
+
+ // 't'ick to 'g'eometric radians is used all over the place here
+ var t2g = function(d) { return ax.t2g(d.x); };
// (x,y) at max radius
function rad2xy(rad) {
return [radius * Math.cos(rad), radius * Math.sin(rad)];
}
- // Set the angular range in degrees to make auto-tick computation cleaner,
- // changing rotation/direction should not affect the angular tick labels.
- if(ax.type === 'linear') {
- if(isFullCircle(sector)) {
- ax.range = sector.slice();
- } else {
- ax.range = sectorInRad.map(ax.unTransformRad).map(rad2deg);
- }
-
- // run rad2deg on tick0 and ditck for thetaunit: 'radians' axes
- if(ax.thetaunit === 'radians') {
- ax.tick0 = rad2deg(ax.tick0);
- ax.dtick = rad2deg(ax.dtick);
- }
-
+ // run rad2deg on tick0 and ditck for thetaunit: 'radians' axes
+ if(ax.type === 'linear' && ax.thetaunit === 'radians') {
+ ax.tick0 = rad2deg(ax.tick0);
+ ax.dtick = rad2deg(ax.dtick);
}
+
// Use tickval filter for category axes instead of tweaking
// the range w.r.t sector, so that sectors that cross 360 can
// show all their ticks.
- else if(ax.type === 'category') {
- var period = angularLayout.period ?
- Math.max(angularLayout.period, angularLayout._categories.length) :
- angularLayout._categories.length;
-
- ax.range = [0, period];
- ax._tickFilter = function(d) { return isAngleInSector(c2rad(d), sector); };
+ if(ax.type === 'category') {
+ ax._tickFilter = function(d) { return isAngleInSector(t2g(d), sector); };
}
- setScale(ax, angularLayout, fullLayout);
-
ax._transfn = function(d) {
- var rad = c2rad(d);
+ var rad = t2g(d);
var xy = rad2xy(rad);
var out = strTranslate(cx + xy[0], cy - xy[1]);
@@ -485,7 +504,7 @@ proto.updateAngularAxis = function(fullLayout, polarLayout) {
};
ax._gridpath = function(d) {
- var rad = c2rad(d);
+ var rad = t2g(d);
var xy = rad2xy(rad);
return 'M0,0L' + (-xy[0]) + ',' + xy[1];
};
@@ -493,7 +512,7 @@ proto.updateAngularAxis = function(fullLayout, polarLayout) {
var offset4fontsize = (angularLayout.ticks !== 'outside' ? 0.7 : 0.5);
ax._labelx = function(d) {
- var rad = c2rad(d);
+ var rad = t2g(d);
var labelStandoff = ax._labelStandoff;
var pad = ax._pad;
@@ -506,7 +525,7 @@ proto.updateAngularAxis = function(fullLayout, polarLayout) {
};
ax._labely = function(d) {
- var rad = c2rad(d);
+ var rad = t2g(d);
var labelStandoff = ax._labelStandoff;
var labelShift = ax._labelShift;
var pad = ax._pad;
@@ -518,7 +537,7 @@ proto.updateAngularAxis = function(fullLayout, polarLayout) {
};
ax._labelanchor = function(angle, d) {
- var rad = c2rad(d);
+ var rad = t2g(d);
return signSin(rad) === 0 ?
(signCos(rad) > 0 ? 'start' : 'end') :
'middle';
@@ -526,17 +545,18 @@ proto.updateAngularAxis = function(fullLayout, polarLayout) {
var newTickLayout = strTickLayout(angularLayout);
if(_this.angularTickLayout !== newTickLayout) {
- layers['angular-axis'].selectAll('.angulartick').remove();
+ layers['angular-axis'].selectAll('.' + ax._id + 'tick').remove();
_this.angularTickLayout = newTickLayout;
}
- Axes.doTicksSingle(gd, ax, true);
+ ax.setScale();
+ doTicksSingle(gd, ax, true);
- // angle of polygon vertices in radians (null means circles)
+ // angle of polygon vertices in geometric radians (null means circles)
// TODO what to do when ax.period > ax._categories ??
var vangles;
if(polarLayout.gridshape === 'linear') {
- vangles = ax._vals.map(c2rad);
+ vangles = ax._vals.map(t2g);
// ax._vals should be always ordered, make them
// always turn counterclockwise for convenience here
@@ -956,7 +976,7 @@ proto.updateRadialDrag = function(fullLayout, polarLayout) {
if((drange > 0) !== (rprime > range0[0])) return;
rng1 = radialAxis.range[1] = rprime;
- Axes.doTicksSingle(gd, _this.radialAxis, true);
+ doTicksSingle(gd, _this.radialAxis, true);
layers['radial-grid']
.attr('transform', strTranslate(cx, cy))
.selectAll('path').attr('transform', null);
@@ -968,15 +988,17 @@ proto.updateRadialDrag = function(fullLayout, polarLayout) {
_this.xaxis.setScale();
_this.yaxis.setScale();
- for(var k in _this.traceHash) {
- var moduleCalcData = _this.traceHash[k];
+ if(_this._scene) _this._scene.clear();
+
+ for(var traceType in _this.traceHash) {
+ var moduleCalcData = _this.traceHash[traceType];
var moduleCalcDataVisible = Lib.filterVisible(moduleCalcData);
var _module = moduleCalcData[0][0].trace._module;
var polarLayoutNow = gd._fullLayout[_this.id];
_module.plot(gd, _this, moduleCalcDataVisible, polarLayoutNow);
- if(!Registry.traceIs(k, 'gl')) {
+ if(!Registry.traceIs(traceType, 'gl')) {
for(var i = 0; i < moduleCalcDataVisible.length; i++) {
_module.style(gd, moduleCalcDataVisible[i]);
}
@@ -1011,6 +1033,7 @@ proto.updateAngularDrag = function(fullLayout, polarLayout) {
var gd = _this.gd;
var layers = _this.layers;
var radius = _this.radius;
+ var angularAxis = _this.angularAxis;
var cx = _this.cx;
var cy = _this.cy;
var cxx = _this.cxx;
@@ -1051,8 +1074,6 @@ proto.updateAngularDrag = function(fullLayout, polarLayout) {
var rot0, rot1;
// induced radial axis rotation (only used on polygon grids)
var rrot1;
- // copy of polar sector value at drag start
- var sector0;
// angle about circle center at drag start
var a0;
@@ -1101,28 +1122,22 @@ proto.updateAngularDrag = function(fullLayout, polarLayout) {
sel.attr('transform', strRotate([da, tx.attr('x'), tx.attr('y')]) + strTranslate(xy.x, xy.y));
});
- var angularAxis = _this.angularAxis;
+ // update rotation -> range -> _m,_b
angularAxis.rotation = wrap180(rot1);
+ angularAxis.setGeometry();
+ angularAxis.setScale();
- if(angularAxis.type === 'linear' && !isFullCircle(sector)) {
- angularAxis.range = sector0
- .map(deg2rad)
- .map(angularAxis.unTransformRad)
- .map(rad2deg);
- }
-
- setConvertAngular(angularAxis);
- Axes.doTicksSingle(gd, angularAxis, true);
+ doTicksSingle(gd, angularAxis, true);
if(_this._hasClipOnAxisFalse && !isFullCircle(sector)) {
- // mutate sector to trick isPtWithinSector
- _this.sector = [sector0[0] - da, sector0[1] - da];
scatterTraces.call(Drawing.hideOutsideRangePoints, _this);
}
- for(var k in _this.traceHash) {
- if(Registry.traceIs(k, 'gl')) {
- var moduleCalcData = _this.traceHash[k];
+ if(_this._scene) _this._scene.clear();
+
+ for(var traceType in _this.traceHash) {
+ if(Registry.traceIs(traceType, 'gl')) {
+ var moduleCalcData = _this.traceHash[traceType];
var moduleCalcDataVisible = Lib.filterVisible(moduleCalcData);
var _module = moduleCalcData[0][0].trace._module;
_module.plot(gd, _this, moduleCalcDataVisible, polarLayoutNow);
@@ -1145,7 +1160,6 @@ proto.updateAngularDrag = function(fullLayout, polarLayout) {
dragOpts.prepFn = function(evt, startX, startY) {
var polarLayoutNow = fullLayout[_this.id];
- sector0 = polarLayoutNow.sector.slice();
rot0 = polarLayoutNow.angularaxis.rotation;
var bbox = angularDrag.getBoundingClientRect();
@@ -1170,8 +1184,9 @@ proto.updateAngularDrag = function(fullLayout, polarLayout) {
proto.isPtWithinSector = function(d) {
var sector = this.sector;
+ var thetag = this.angularAxis.c2g(d.theta);
- if(!isAngleInSector(d.rad, sector)) {
+ if(!isAngleInSector(thetag, sector)) {
return false;
}
@@ -1192,7 +1207,7 @@ proto.isPtWithinSector = function(d) {
if(vangles) {
var polygonIn = polygonTester(makePolygon(r0, sector, vangles));
var polygonOut = polygonTester(makePolygon(r1, sector, vangles));
- var xy = [r * Math.cos(d.rad), r * Math.sin(d.rad)];
+ var xy = [r * Math.cos(thetag), r * Math.sin(thetag)];
return polygonOut.contains(xy) && !polygonIn.contains(xy);
}
@@ -1205,11 +1220,6 @@ proto.fillViewInitialKey = function(key, val) {
}
};
-function setScale(ax, axLayout, fullLayout) {
- Axes.setConvert(ax, fullLayout);
- ax.setScale();
-}
-
function strTickLayout(axLayout) {
var out = axLayout.ticks + String(axLayout.ticklen) + String(axLayout.showticklabels);
if('side' in axLayout) out += axLayout.side;
@@ -1541,10 +1551,6 @@ function pathAnnulus(r0, r1, sector) {
}
}
-function isFullCircle(sector) {
- var arc = Math.abs(sector[1] - sector[0]);
- return arc === 360;
-}
function updateElement(sel, showAttr, attrs) {
if(showAttr) {
diff --git a/src/plots/polar/set_convert.js b/src/plots/polar/set_convert.js
new file mode 100644
index 00000000000..d7b12f5d679
--- /dev/null
+++ b/src/plots/polar/set_convert.js
@@ -0,0 +1,180 @@
+/**
+* Copyright 2012-2018, 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 Lib = require('../../lib');
+var setConvertCartesian = require('../cartesian/set_convert');
+
+var deg2rad = Lib.deg2rad;
+var rad2deg = Lib.rad2deg;
+var isFullCircle = Lib.isFullCircle;
+
+/**
+ * setConvert for polar axes!
+ *
+ * @param {object} ax
+ * axis in question (works for both radial and angular axes)
+ * @param {object} polarLayout
+ * full polar layout of the subplot associated with 'ax'
+ * @param {object} fullLayout
+ * full layout
+ *
+ * Here, reuse some of the Cartesian setConvert logic,
+ * but we must extend some of it, as both radial and angular axes
+ * don't have domains and angular axes don't have _true_ ranges.
+ *
+ * Moreover, we introduce two new coordinate systems:
+ * - 'g' for geometric coordinates and
+ * - 't' for angular ticks
+ *
+ * Radial axis coordinate systems:
+ * - d, c and l: same as for cartesian axes
+ * - g: like calcdata but translated about `radialaxis.range[0]`
+ *
+ * Angular axis coordinate systems:
+ * - d: data, in whatever form it's provided
+ * - c: calcdata, turned into radians (for linear axes)
+ * or category indices (category axes)
+ * - t: tick calcdata, just like 'c' but in degrees for linear axes
+ * - g: geometric calcdata, radians coordinates that take into account
+ * axis rotation and direction
+ *
+ * Then, 'g'eometric data is ready to be converted to (x,y).
+ */
+module.exports = function setConvert(ax, polarLayout, fullLayout) {
+ setConvertCartesian(ax, fullLayout);
+
+ switch(ax._id) {
+ case 'x':
+ case 'radialaxis':
+ setConvertRadial(ax);
+ break;
+ case 'angularaxis':
+ setConvertAngular(ax, polarLayout);
+ break;
+ }
+};
+
+function setConvertRadial(ax) {
+ ax.setGeometry = function() {
+ var rng = ax.range;
+
+ var rFilter = rng[0] > rng[1] ?
+ function(v) { return v <= 0; } :
+ function(v) { return v >= 0; };
+
+ ax.c2g = function(v) {
+ var r = ax.c2r(v) - rng[0];
+ return rFilter(r) ? r : 0;
+ };
+
+ ax.g2c = function(v) {
+ return ax.r2c(v + rng[0]);
+ };
+ };
+}
+
+function toRadians(v, unit) {
+ return unit === 'degrees' ? deg2rad(v) : v;
+}
+
+function fromRadians(v, unit) {
+ return unit === 'degrees' ? rad2deg(v) : v;
+}
+
+function setConvertAngular(ax, polarLayout) {
+ var axType = ax.type;
+
+ if(axType === 'linear') {
+ var _d2c = ax.d2c;
+ var _c2d = ax.c2d;
+
+ ax.d2c = function(v, unit) { return toRadians(_d2c(v), unit); };
+ ax.c2d = function(v, unit) { return _c2d(fromRadians(v, unit)); };
+ }
+
+ // override makeCalcdata to handle thetaunit and special theta0/dtheta logic
+ ax.makeCalcdata = function(trace, coord) {
+ var arrayIn = trace[coord];
+ var len = trace._length;
+ var arrayOut, i;
+
+ var _d2c = function(v) { return ax.d2c(v, trace.thetaunit); };
+
+ if(arrayIn) {
+ if(Lib.isTypedArray(arrayIn) && axType === 'linear') {
+ if(len === arrayIn.length) {
+ return arrayIn;
+ } else if(arrayIn.subarray) {
+ return arrayIn.subarray(0, len);
+ }
+ }
+
+ arrayOut = new Array(len);
+ for(i = 0; i < len; i++) {
+ arrayOut[i] = _d2c(arrayIn[i]);
+ }
+ } else {
+ var coord0 = coord + '0';
+ var dcoord = 'd' + coord;
+ var v0 = (coord0 in trace) ? _d2c(trace[coord0]) : 0;
+ var dv = (trace[dcoord]) ? _d2c(trace[dcoord]) : (ax.period || 2 * Math.PI) / len;
+
+ arrayOut = new Array(len);
+ for(i = 0; i < len; i++) {
+ arrayOut[i] = v0 + i * dv;
+ }
+ }
+
+ return arrayOut;
+ };
+
+ // N.B. we mock the axis 'range' here
+ ax.setGeometry = function() {
+ var sector = polarLayout.sector;
+ var dir = {clockwise: -1, counterclockwise: 1}[ax.direction];
+ var rot = deg2rad(ax.rotation);
+
+ var rad2g = function(v) { return dir * v + rot; };
+ var g2rad = function(v) { return (v - rot) / dir; };
+
+ var rad2c, c2rad;
+ var rad2t, t2rad;
+
+ switch(axType) {
+ case 'linear':
+ c2rad = rad2c = Lib.identity;
+ t2rad = deg2rad;
+ rad2t = rad2deg;
+
+ // Set the angular range in degrees to make auto-tick computation cleaner,
+ // changing rotation/direction should not affect the angular tick value.
+ ax.range = isFullCircle(sector) ?
+ sector.slice() :
+ sector.map(deg2rad).map(g2rad).map(rad2deg);
+ break;
+
+ case 'category':
+ var catLen = ax._categories.length;
+ var _period = ax.period ? Math.max(ax.period, catLen) : catLen;
+
+ c2rad = t2rad = function(v) { return v * 2 * Math.PI / _period; };
+ rad2c = rad2t = function(v) { return v * _period / Math.PI / 2; };
+
+ ax.range = [0, _period];
+ break;
+ }
+
+ ax.c2g = function(v) { return rad2g(c2rad(v)); };
+ ax.g2c = function(v) { return rad2c(g2rad(v)); };
+
+ ax.t2g = function(v) { return rad2g(t2rad(v)); };
+ ax.g2t = function(v) { return rad2t(g2rad(v)); };
+ };
+}
diff --git a/src/traces/bar/calc.js b/src/traces/bar/calc.js
index 10fc320af06..b3c5775d5e0 100644
--- a/src/traces/bar/calc.js
+++ b/src/traces/bar/calc.js
@@ -15,17 +15,11 @@ var arraysToCalcdata = require('./arrays_to_calcdata');
var calcSelection = require('../scatter/calc_selection');
module.exports = function calc(gd, trace) {
- // depending on bar direction, set position and size axes
- // and data ranges
- // note: this logic for choosing orientation is
- // duplicated in graph_obj->setstyles
+ var xa = Axes.getFromId(gd, trace.xaxis || 'x');
+ var ya = Axes.getFromId(gd, trace.yaxis || 'y');
+ var size, pos;
- var xa = Axes.getFromId(gd, trace.xaxis || 'x'),
- ya = Axes.getFromId(gd, trace.yaxis || 'y'),
- orientation = trace.orientation || ((trace.x && !trace.y) ? 'h' : 'v'),
- pos, size, i;
-
- if(orientation === 'h') {
+ if(trace.orientation === 'h') {
size = xa.makeCalcdata(trace, 'x');
pos = ya.makeCalcdata(trace, 'y');
} else {
@@ -38,7 +32,7 @@ module.exports = function calc(gd, trace) {
var cd = new Array(serieslen);
// set position and size
- for(i = 0; i < serieslen; i++) {
+ for(var i = 0; i < serieslen; i++) {
cd[i] = { p: pos[i], s: size[i] };
if(trace.ids) {
diff --git a/src/traces/scatter/line_points.js b/src/traces/scatter/line_points.js
index ef84cb0cdaa..8e498e46466 100644
--- a/src/traces/scatter/line_points.js
+++ b/src/traces/scatter/line_points.js
@@ -59,7 +59,7 @@ module.exports = function linePoints(d, opts) {
if(!di) return false;
var x = xa.c2p(di.x);
var y = ya.c2p(di.y);
- if(x === BADNUM || y === BADNUM) return di.intoCenter || false;
+ if(x === BADNUM || y === BADNUM) return false;
return [x, y];
}
diff --git a/src/traces/scattergl/attributes.js b/src/traces/scattergl/attributes.js
index a4d709d10c8..3d8798115bd 100644
--- a/src/traces/scattergl/attributes.js
+++ b/src/traces/scattergl/attributes.js
@@ -78,7 +78,7 @@ var attrs = module.exports = overrideAll({
fill: scatterAttrs.fill,
fillcolor: scatterAttrs.fillcolor,
- hoveron: scatterAttrs.hoveron,
+ // no hoveron
selected: {
marker: scatterAttrs.selected.marker,
diff --git a/src/traces/scattergl/defaults.js b/src/traces/scattergl/defaults.js
index 328c50e8709..2703cb31b20 100644
--- a/src/traces/scattergl/defaults.js
+++ b/src/traces/scattergl/defaults.js
@@ -44,12 +44,9 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce);
}
- var dfltHoverOn = [];
-
if(subTypes.hasMarkers(traceOut)) {
handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce);
coerce('marker.line.width', isOpen || isBubble ? 1 : 0);
- dfltHoverOn.push('points');
}
if(subTypes.hasText(traceOut)) {
@@ -61,12 +58,6 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
handleFillColorDefaults(traceIn, traceOut, defaultColor, coerce);
}
- if(traceOut.fill === 'tonext' || traceOut.fill === 'toself') {
- dfltHoverOn.push('fills');
- }
-
- coerce('hoveron', dfltHoverOn.join('+') || 'points');
-
var errorBarsSupplyDefaults = Registry.getComponentMethod('errorbars', 'supplyDefaults');
errorBarsSupplyDefaults(traceIn, traceOut, defaultColor, {axis: 'y'});
errorBarsSupplyDefaults(traceIn, traceOut, defaultColor, {axis: 'x', inherit: 'y'});
diff --git a/src/traces/scattergl/index.js b/src/traces/scattergl/index.js
index e4cf251a984..f96b1458fd5 100644
--- a/src/traces/scattergl/index.js
+++ b/src/traces/scattergl/index.js
@@ -300,11 +300,10 @@ function sceneUpdate(gd, subplot) {
if(scene.select2d) {
clearViewport(scene.select2d, vp);
}
- if(scene.scatter2d) {
- clearViewport(scene.scatter2d, vp);
- } else if(scene.glText) {
- clearViewport(scene.glText[0], vp);
- }
+
+ var anyComponent = scene.scatter2d || scene.line2d ||
+ (scene.glText || [])[0] || scene.fill2d || scene.error2d;
+ if(anyComponent) clearViewport(anyComponent, vp);
};
// remove scene resources
diff --git a/src/traces/scatterpolar/attributes.js b/src/traces/scatterpolar/attributes.js
index 576780bd047..e05c8f31da3 100644
--- a/src/traces/scatterpolar/attributes.js
+++ b/src/traces/scatterpolar/attributes.js
@@ -28,6 +28,49 @@ module.exports = {
description: 'Sets the angular coordinates'
},
+ r0: {
+ valType: 'any',
+ dflt: 0,
+ role: 'info',
+ editType: 'calc+clearAxisTypes',
+ description: [
+ 'Alternate to `r`.',
+ 'Builds a linear space of r coordinates.',
+ 'Use with `dr`',
+ 'where `r0` is the starting coordinate and `dr` the step.'
+ ].join(' ')
+ },
+ dr: {
+ valType: 'number',
+ dflt: 1,
+ role: 'info',
+ editType: 'calc',
+ description: 'Sets the r coordinate step.'
+ },
+
+ theta0: {
+ valType: 'any',
+ dflt: 0,
+ role: 'info',
+ editType: 'calc+clearAxisTypes',
+ description: [
+ 'Alternate to `theta`.',
+ 'Builds a linear space of theta coordinates.',
+ 'Use with `dtheta`',
+ 'where `theta0` is the starting coordinate and `dtheta` the step.'
+ ].join(' ')
+ },
+ dtheta: {
+ valType: 'number',
+ role: 'info',
+ editType: 'calc',
+ description: [
+ 'Sets the theta coordinate step.',
+ 'By default, the `dtheta` step equals the subplot\'s period divided',
+ 'by the length of the `r` coordinates.'
+ ].join(' ')
+ },
+
thetaunit: {
valType: 'enumerated',
values: ['radians', 'degrees', 'gradians'],
diff --git a/src/traces/scatterpolar/calc.js b/src/traces/scatterpolar/calc.js
index f2f10dd7f2d..69d1cd55600 100644
--- a/src/traces/scatterpolar/calc.js
+++ b/src/traces/scatterpolar/calc.js
@@ -29,10 +29,6 @@ module.exports = function calc(gd, trace) {
var len = trace._length;
var cd = new Array(len);
- function c2rad(v) {
- return angularAxis.c2rad(v, trace.thetaunit);
- }
-
for(var i = 0; i < len; i++) {
var r = rArray[i];
var theta = thetaArray[i];
@@ -41,7 +37,6 @@ module.exports = function calc(gd, trace) {
if(isNumeric(r) && isNumeric(theta)) {
cdi.r = r;
cdi.theta = theta;
- cdi.rad = c2rad(theta);
} else {
cdi.r = BADNUM;
}
diff --git a/src/traces/scatterpolar/defaults.js b/src/traces/scatterpolar/defaults.js
index 5fe19016b48..1e3a47f02ce 100644
--- a/src/traces/scatterpolar/defaults.js
+++ b/src/traces/scatterpolar/defaults.js
@@ -20,22 +20,17 @@ var PTS_LINESONLY = require('../scatter/constants').PTS_LINESONLY;
var attributes = require('./attributes');
-module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
+function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
function coerce(attr, dflt) {
return Lib.coerce(traceIn, traceOut, attributes, attr, dflt);
}
- var r = coerce('r');
- var theta = coerce('theta');
- var len = (r && theta) ? Math.min(r.length, theta.length) : 0;
-
+ var len = handleRThetaDefaults(traceIn, traceOut, layout, coerce);
if(!len) {
traceOut.visible = false;
return;
}
- traceOut._length = len;
-
coerce('thetaunit');
coerce('mode', len < PTS_LINESONLY ? 'lines+markers' : 'lines');
coerce('text');
@@ -76,4 +71,33 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
coerce('hoveron', dfltHoverOn.join('+') || 'points');
Lib.coerceSelectionMarkerOpacity(traceOut, coerce);
+}
+
+function handleRThetaDefaults(traceIn, traceOut, layout, coerce) {
+ var r = coerce('r');
+ var theta = coerce('theta');
+ var len;
+
+ if(r) {
+ if(theta) {
+ len = Math.min(r.length, theta.length);
+ } else {
+ len = r.length;
+ coerce('theta0');
+ coerce('dtheta');
+ }
+ } else {
+ if(!theta) return 0;
+ len = traceOut.theta.length;
+ coerce('r0');
+ coerce('dr');
+ }
+
+ traceOut._length = len;
+ return len;
+}
+
+module.exports = {
+ handleRThetaDefaults: handleRThetaDefaults,
+ supplyDefaults: supplyDefaults
};
diff --git a/src/traces/scatterpolar/hover.js b/src/traces/scatterpolar/hover.js
index 8202724a332..6eaad2950c0 100644
--- a/src/traces/scatterpolar/hover.js
+++ b/src/traces/scatterpolar/hover.js
@@ -46,23 +46,21 @@ function makeHoverPointText(cdi, trace, subplot) {
radialAxis._hovertitle = 'r';
angularAxis._hovertitle = 'θ';
- var rad = angularAxis._c2rad(cdi.theta, trace.thetaunit);
-
- // show theta value in unit of angular axis
- var theta;
- if(angularAxis.type === 'linear' && trace.thetaunit !== angularAxis.thetaunit) {
- theta = angularAxis.thetaunit === 'degrees' ? Lib.rad2deg(rad) : rad;
- } else {
- theta = cdi.theta;
- }
-
function textPart(ax, val) {
text.push(ax._hovertitle + ': ' + Axes.tickText(ax, val, 'hover').text);
}
if(parts.indexOf('all') !== -1) parts = ['r', 'theta'];
- if(parts.indexOf('r') !== -1) textPart(radialAxis, radialAxis.c2r(cdi.r));
- if(parts.indexOf('theta') !== -1) textPart(angularAxis, theta);
+ if(parts.indexOf('r') !== -1) {
+ textPart(radialAxis, radialAxis.c2r(cdi.r));
+ }
+ if(parts.indexOf('theta') !== -1) {
+ var theta = cdi.theta;
+ textPart(
+ angularAxis,
+ angularAxis.thetaunit === 'degrees' ? Lib.rad2deg(theta) : theta
+ );
+ }
return text.join('
');
}
diff --git a/src/traces/scatterpolar/index.js b/src/traces/scatterpolar/index.js
index ece30328990..8a3f55e609a 100644
--- a/src/traces/scatterpolar/index.js
+++ b/src/traces/scatterpolar/index.js
@@ -15,7 +15,7 @@ module.exports = {
categories: ['polar', 'symbols', 'showLegend', 'scatter-like'],
attributes: require('./attributes'),
- supplyDefaults: require('./defaults'),
+ supplyDefaults: require('./defaults').supplyDefaults,
colorbar: require('../scatter/marker_colorbar'),
calc: require('./calc'),
plot: require('./plot'),
diff --git a/src/traces/scatterpolar/plot.js b/src/traces/scatterpolar/plot.js
index e0018ddfc85..cde1dd613be 100644
--- a/src/traces/scatterpolar/plot.js
+++ b/src/traces/scatterpolar/plot.js
@@ -12,7 +12,7 @@ var scatterPlot = require('../scatter/plot');
var BADNUM = require('../../constants/numerical').BADNUM;
module.exports = function plot(gd, subplot, moduleCalcData) {
- var i, j;
+ var mlayer = subplot.layers.frontplot.select('g.scatterlayer');
var plotinfo = {
xaxis: subplot.xaxis,
@@ -22,45 +22,27 @@ module.exports = function plot(gd, subplot, moduleCalcData) {
};
var radialAxis = subplot.radialAxis;
- var radialRange = radialAxis.range;
- var rFilter;
-
- if(radialRange[0] > radialRange[1]) {
- rFilter = function(v) { return v <= 0; };
- } else {
- rFilter = function(v) { return v >= 0; };
- }
-
- // map (r, theta) first to a 'geometric' r and then to (x,y)
- // on-par with what scatterPlot expects.
-
- for(i = 0; i < moduleCalcData.length; i++) {
- for(j = 0; j < moduleCalcData[i].length; j++) {
- var cdi = moduleCalcData[i][j];
- var r = cdi.r;
-
- if(r !== BADNUM) {
- // convert to 'r' data to fit with mocked polar x/y axis
- // which are always `type: 'linear'`
- var rr = radialAxis.c2r(r) - radialRange[0];
- if(rFilter(rr)) {
- var rad = cdi.rad;
- cdi.x = rr * Math.cos(rad);
- cdi.y = rr * Math.sin(rad);
- continue;
- } else {
- // flag for scatter/line_points.js
- // to extend line (and fills) into center
- cdi.intoCenter = [subplot.cxx, subplot.cyy];
- }
+ var angularAxis = subplot.angularAxis;
+
+ // convert:
+ // 'c' (r,theta) -> 'geometric' (r,theta) -> (x,y)
+ for(var i = 0; i < moduleCalcData.length; i++) {
+ var cdi = moduleCalcData[i];
+
+ for(var j = 0; j < cdi.length; j++) {
+ var cd = cdi[j];
+ var r = cd.r;
+
+ if(r === BADNUM) {
+ cd.x = cd.y = BADNUM;
+ } else {
+ var rg = radialAxis.c2g(r);
+ var thetag = angularAxis.c2g(cd.theta);
+ cd.x = rg * Math.cos(thetag);
+ cd.y = rg * Math.sin(thetag);
}
-
- cdi.x = BADNUM;
- cdi.y = BADNUM;
}
}
- var scatterLayer = subplot.layers.frontplot.select('g.scatterlayer');
-
- scatterPlot(gd, plotinfo, moduleCalcData, scatterLayer);
+ scatterPlot(gd, plotinfo, moduleCalcData, mlayer);
};
diff --git a/src/traces/scatterpolargl/attributes.js b/src/traces/scatterpolargl/attributes.js
index 6f1ce8c6dc4..fbcbaa1a415 100644
--- a/src/traces/scatterpolargl/attributes.js
+++ b/src/traces/scatterpolargl/attributes.js
@@ -15,10 +15,14 @@ module.exports = {
mode: scatterPolarAttrs.mode,
r: scatterPolarAttrs.r,
theta: scatterPolarAttrs.theta,
+ r0: scatterPolarAttrs.r0,
+ dr: scatterPolarAttrs.dr,
+ theta0: scatterPolarAttrs.theta0,
+ dtheta: scatterPolarAttrs.dtheta,
thetaunit: scatterPolarAttrs.thetaunit,
text: scatterPolarAttrs.text,
- // no hovertext
+ hovertext: scatterPolarAttrs.hovertext,
line: scatterGlAttrs.line,
connectgaps: scatterGlAttrs.connectgaps,
@@ -29,8 +33,11 @@ module.exports = {
fill: scatterGlAttrs.fill,
fillcolor: scatterGlAttrs.fillcolor,
+ textposition: scatterGlAttrs.textposition,
+ textfont: scatterGlAttrs.textfont,
+
hoverinfo: scatterPolarAttrs.hoverinfo,
- hoveron: scatterPolarAttrs.hoveron,
+ // no hoveron
selected: scatterPolarAttrs.selected,
unselected: scatterPolarAttrs.unselected
diff --git a/src/traces/scatterpolargl/defaults.js b/src/traces/scatterpolargl/defaults.js
index 1c6a5cae2c3..70efac6da99 100644
--- a/src/traces/scatterpolargl/defaults.js
+++ b/src/traces/scatterpolargl/defaults.js
@@ -11,8 +11,10 @@
var Lib = require('../../lib');
var subTypes = require('../scatter/subtypes');
+var handleRThetaDefaults = require('../scatterpolar/defaults').handleRThetaDefaults;
var handleMarkerDefaults = require('../scatter/marker_defaults');
var handleLineDefaults = require('../scatter/line_defaults');
+var handleTextDefaults = require('../scatter/text_defaults');
var handleFillColorDefaults = require('../scatter/fillcolor_defaults');
var PTS_LINESONLY = require('../scatter/constants').PTS_LINESONLY;
@@ -23,31 +25,28 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
return Lib.coerce(traceIn, traceOut, attributes, attr, dflt);
}
- var r = coerce('r');
- var theta = coerce('theta');
- var len = (r && theta) ? Math.min(r.length, theta.length) : 0;
-
+ var len = handleRThetaDefaults(traceIn, traceOut, layout, coerce);
if(!len) {
traceOut.visible = false;
return;
}
- traceOut._length = len;
-
coerce('thetaunit');
coerce('mode', len < PTS_LINESONLY ? 'lines+markers' : 'lines');
coerce('text');
+ coerce('hovertext');
if(subTypes.hasLines(traceOut)) {
handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce);
coerce('connectgaps');
}
- var dfltHoverOn = [];
-
if(subTypes.hasMarkers(traceOut)) {
handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce);
- dfltHoverOn.push('points');
+ }
+
+ if(subTypes.hasText(traceOut)) {
+ handleTextDefaults(traceIn, traceOut, layout, coerce);
}
coerce('fill');
@@ -55,10 +54,5 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
handleFillColorDefaults(traceIn, traceOut, defaultColor, coerce);
}
- if(traceOut.fill === 'tonext' || traceOut.fill === 'toself') {
- dfltHoverOn.push('fills');
- }
- coerce('hoveron', dfltHoverOn.join('+') || 'points');
-
Lib.coerceSelectionMarkerOpacity(traceOut, coerce);
};
diff --git a/src/traces/scatterpolargl/index.js b/src/traces/scatterpolargl/index.js
index b5a16ed6937..b2e6be07249 100644
--- a/src/traces/scatterpolargl/index.js
+++ b/src/traces/scatterpolargl/index.js
@@ -44,10 +44,8 @@ function calc(container, trace) {
function plot(container, subplot, cdata) {
var radialAxis = subplot.radialAxis;
var angularAxis = subplot.angularAxis;
- var rRange = radialAxis.range;
var scene = ScatterGl.sceneUpdate(container, subplot);
- scene.clear();
cdata.forEach(function(cdscatter, traceIndex) {
if(!cdscatter || !cdscatter[0] || !cdscatter[0].trace) return;
@@ -56,42 +54,38 @@ function plot(container, subplot, cdata) {
var stash = cd.t;
var rArray = stash.r;
var thetaArray = stash.theta;
- var i, r, rr, theta, rad;
+ var i;
var subRArray = rArray.slice();
var subThetaArray = thetaArray.slice();
// filter out by range
for(i = 0; i < rArray.length; i++) {
- r = rArray[i], theta = thetaArray[i];
- rad = angularAxis.c2rad(theta, trace.thetaunit);
-
- if(!subplot.isPtWithinSector({r: r, rad: rad})) {
+ if(!subplot.isPtWithinSector({r: rArray[i], theta: thetaArray[i]})) {
subRArray[i] = NaN;
subThetaArray[i] = NaN;
}
}
var count = rArray.length;
- var positions = new Array(count * 2), x = Array(count), y = Array(count);
-
- function c2rad(v) {
- return angularAxis.c2rad(v, trace.thetaunit);
- }
+ var positions = new Array(count * 2);
+ var x = Array(count);
+ var y = Array(count);
for(i = 0; i < count; i++) {
- r = subRArray[i];
- theta = subThetaArray[i];
-
- if(isNumeric(r) && isNumeric(theta) && r >= 0) {
- rr = radialAxis.c2r(r) - rRange[0];
- rad = c2rad(theta);
-
- x[i] = positions[i * 2] = rr * Math.cos(rad);
- y[i] = positions[i * 2 + 1] = rr * Math.sin(rad);
+ var r = subRArray[i];
+ var xx, yy;
+
+ if(isNumeric(r)) {
+ var rg = radialAxis.c2g(r);
+ var thetag = angularAxis.c2g(subThetaArray[i], trace.thetaunit);
+ xx = rg * Math.cos(thetag);
+ yy = rg * Math.sin(thetag);
} else {
- x[i] = y[i] = positions[i * 2] = positions[i * 2 + 1] = NaN;
+ xx = yy = NaN;
}
+ x[i] = positions[i * 2] = xx;
+ y[i] = positions[i * 2 + 1] = yy;
}
var options = ScatterGl.sceneOptions(container, subplot, trace, positions);
@@ -101,6 +95,7 @@ function plot(container, subplot, cdata) {
if(options.marker && !scene.scatter2d) scene.scatter2d = true;
if(options.line && !scene.line2d) scene.line2d = true;
if((options.errorX || options.errorY) && !scene.error2d) scene.error2d = true;
+ if(options.text && !scene.glText) scene.glText = true;
stash.tree = cluster(positions);
@@ -122,6 +117,9 @@ function plot(container, subplot, cdata) {
scene.markerOptions.push(options.marker);
scene.markerSelectedOptions.push(options.markerSel);
scene.markerUnselectedOptions.push(options.markerUnsel);
+ scene.textOptions.push(options.text);
+ scene.textSelectedOptions.push(options.textSel);
+ scene.textUnselectedOptions.push(options.textUnsel);
scene.count = cdata.length;
// stash scene ref
@@ -156,14 +154,12 @@ function hoverPoints(pointData, xval, yval, hovermode) {
}
var subplot = pointData.subplot;
- var angularAxis = subplot.angularAxis;
var cdi = newPointData.cd[newPointData.index];
var trace = newPointData.trace;
// augment pointData with r/theta param
cdi.r = rArray[newPointData.index];
cdi.theta = thetaArray[newPointData.index];
- cdi.rad = angularAxis.c2rad(cdi.theta, trace.thetaunit);
if(!subplot.isPtWithinSector(cdi)) return;
diff --git a/test/image/baselines/glpolar_subplots.png b/test/image/baselines/glpolar_subplots.png
new file mode 100644
index 00000000000..ed807c1ab15
Binary files /dev/null and b/test/image/baselines/glpolar_subplots.png differ
diff --git a/test/image/baselines/polar_r0dr-theta0dtheta.png b/test/image/baselines/polar_r0dr-theta0dtheta.png
new file mode 100644
index 00000000000..4345d2092ee
Binary files /dev/null and b/test/image/baselines/polar_r0dr-theta0dtheta.png differ
diff --git a/test/image/mocks/glpolar_subplots.json b/test/image/mocks/glpolar_subplots.json
new file mode 100644
index 00000000000..7b689f4786b
--- /dev/null
+++ b/test/image/mocks/glpolar_subplots.json
@@ -0,0 +1,68 @@
+{
+ "data": [{
+ "type": "scatterpolargl",
+ "mode": "markers+lines+text",
+ "r": [1, 2, 3],
+ "theta": [0, 45, 180],
+ "text": ["A0", "B0", "C0"],
+ "hovertext": ["hover A0", "hover B0", "hover C0"],
+ "marker": {"symbol": "square", "size": 15},
+ "textfont": {
+ "color": ["red", "green", "blue"],
+ "size": 20
+ },
+ "textposition": ["top left", "bottom right", "bottom right"]
+ }, {
+ "type": "scatterpolargl",
+ "mode": "markers+lines+text",
+ "r": [1, 2, 3],
+ "theta": [-0, -45, -180],
+ "text": ["A1", "B1", "C1"],
+ "hovertext": ["hover A1", "hover B1", "hover C1"],
+ "marker": {
+ "symbol": "square",
+ "size": 15,
+ "color": [-1, 1, 2],
+ "showscale": true
+ },
+ "textfont": {
+ "color": "green",
+ "size": [20, 15, 10]
+ },
+ "textposition": "top left",
+ "subplot": "polar2"
+ }],
+
+ "layout": {
+ "showlegend": false,
+ "polar": {
+ "domain": {
+ "x": [0, 0.44]
+ },
+ "radialaxis": {
+ "showgrid": false,
+ "title": "1st subplot",
+ "titlefont": {
+ "size": 20,
+ "color": "blue"
+ },
+ "angle": -45
+ }
+ },
+ "polar2": {
+ "domain": {
+ "x": [0.56, 1]
+ },
+ "radialaxis": {
+ "range": [0, 4.7],
+ "showgrid": false,
+ "title": "2nd subplot",
+ "titlefont": {
+ "size": 10,
+ "color": "orange"
+ },
+ "angle": 45
+ }
+ }
+ }
+}
diff --git a/test/image/mocks/polar_r0dr-theta0dtheta.json b/test/image/mocks/polar_r0dr-theta0dtheta.json
new file mode 100644
index 00000000000..dd2a4fa32c5
--- /dev/null
+++ b/test/image/mocks/polar_r0dr-theta0dtheta.json
@@ -0,0 +1,77 @@
+{
+ "data": [{
+ "type": "scatterpolar",
+ "name": "missing θ",
+ "r": [1, 2, 3]
+ }, {
+ "type": "scatterpolar",
+ "name": "missing r",
+ "theta": [0, -90, -220]
+ }, {
+ "type": "scatterpolar",
+ "name": "set r0/dr",
+ "r0": 5,
+ "dr": -1,
+ "theta": [0, -90, -190]
+ }, {
+ "type": "scatterpolar",
+ "name": "set θ0/dθ",
+ "r": [1, 2, 3],
+ "theta0": 1,
+ "dtheta": 2,
+ "thetaunit": "radians"
+ },
+
+ {
+ "type": "scatterpolargl",
+ "name": "[gl] missing θ",
+ "subplot": "polar2",
+ "marker": {"color": "#1f77b4"},
+ "r": [1, 2, 3]
+ }, {
+ "type": "scatterpolargl",
+ "name": "[gl] missing r",
+ "subplot": "polar2",
+ "marker": {"color": "#ff7f0e"},
+ "theta": [0, -90, -220]
+ }, {
+ "type": "scatterpolargl",
+ "name": "[gl] set r0/dr",
+ "subplot": "polar2",
+ "marker": {"color": "#2ca02c"},
+ "r0": 5,
+ "dr": -1,
+ "theta": [0, -90, -190]
+ }, {
+ "type": "scatterpolargl",
+ "name": "[gl] set θ0/dθ",
+ "subplot": "polar2",
+ "marker": {"color": "#d62728"},
+ "r": [1, 2, 3],
+ "theta0": 1,
+ "dtheta": 2,
+ "thetaunit": "radians"
+ }],
+ "layout": {
+ "width": 500,
+ "height": 800,
+ "margin": {"l": 200, "t": 20, "b": 20},
+ "legend": {
+ "x": -0.2,
+ "xanchor": "right",
+ "y": 0.5
+ },
+ "polar": {
+ "domain": {
+ "x": [0, 1],
+ "y": [0.5, 1]
+ }
+ },
+ "polar2": {
+ "domain": {
+ "x": [0, 1],
+ "y": [0, 0.5]
+ }
+ }
+ }
+}
diff --git a/test/jasmine/tests/polar_test.js b/test/jasmine/tests/polar_test.js
index f7fb263a07c..ee88b681172 100644
--- a/test/jasmine/tests/polar_test.js
+++ b/test/jasmine/tests/polar_test.js
@@ -306,14 +306,14 @@ describe('Test relayout on polar subplots:', function() {
var pos1 = [];
Plotly.plot(gd, fig).then(function() {
- d3.selectAll('.angulartick> text').each(function() {
+ d3.selectAll('.angularaxistick > text').each(function() {
var tx = d3.select(this);
pos0.push([tx.attr('x'), tx.attr('y')]);
});
return Plotly.relayout(gd, 'polar.angularaxis.rotation', 90);
})
.then(function() {
- d3.selectAll('.angulartick> text').each(function() {
+ d3.selectAll('.angularaxistick > text').each(function() {
var tx = d3.select(this);
pos1.push([tx.attr('x'), tx.attr('y')]);
});
@@ -330,7 +330,7 @@ describe('Test relayout on polar subplots:', function() {
var fig = Lib.extendDeep({}, require('@mocks/polar_scatter.json'));
function check(cnt, expected) {
- var ticks = d3.selectAll('path.angulartick');
+ var ticks = d3.selectAll('path.angularaxistick');
expect(ticks.size()).toBe(cnt, '# of ticks');
ticks.each(function() {
@@ -433,21 +433,21 @@ describe('Test relayout on polar subplots:', function() {
return toggle(
'polar.angularaxis.showgrid',
[true, false], [8, 0],
- '.angular-grid > .angular > path', assertCnt
+ '.angular-grid > .angularaxis > path', assertCnt
);
})
.then(function() {
return toggle(
'polar.angularaxis.showticklabels',
[true, false], [8, 0],
- '.angular-axis > .angulartick > text', assertCnt
+ '.angular-axis > .angularaxistick > text', assertCnt
);
})
.then(function() {
return toggle(
'polar.angularaxis.ticks',
['outside', ''], [8, 0],
- '.angular-axis > path.angulartick', assertCnt
+ '.angular-axis > path.angularaxistick', assertCnt
);
})
.catch(failTest)
@@ -558,7 +558,7 @@ describe('Test relayout on polar subplots:', function() {
expect(gd._fullLayout.polar._subplot.angularAxis.range)
.toBeCloseToArray([0, exp.period], 2, 'range in mocked angular axis - ' + msg);
- expect(d3.selectAll('path.angulartick').size())
+ expect(d3.selectAll('path.angularaxistick').size())
.toBe(exp.nTicks, '# of visible angular ticks - ' + msg);
expect([gd.calcdata[0][5].x, gd.calcdata[0][5].y])
@@ -1076,6 +1076,101 @@ describe('Test polar interactions:', function() {
.catch(failTest)
.then(done);
});
+
+ describe('@gl should update scene during drag interactions on radial and angular drag area', function() {
+ var objs = ['scatter2d', 'line2d'];
+ var scene, gl, nTraces;
+
+ function _dragRadial() {
+ var node = d3.select('.polar > .draglayer > .radialdrag').node();
+ var p0 = [375, 200];
+ var dp = [-50, 0];
+ return drag(node, dp[0], dp[1], null, p0[0], p0[1], 2);
+ }
+
+ function _dragAngular() {
+ var node = d3.select('.polar > .draglayer > .angulardrag').node();
+ var p0 = [350, 150];
+ var dp = [-20, 20];
+ return drag(node, dp[0], dp[1], null, p0[0], p0[1]);
+ }
+
+ // once on drag, once on mouseup relayout
+ function _assert() {
+ expect(gl.clear).toHaveBeenCalledTimes(2);
+ gl.clear.calls.reset();
+
+ objs.forEach(function(o) {
+ if(scene[o]) {
+ expect(scene[o].draw).toHaveBeenCalledTimes(2 * nTraces);
+ scene[o].draw.calls.reset();
+ }
+ });
+ }
+
+ var specs = [{
+ desc: 'scatter marker case',
+ // mode: 'markers' by default
+ }, {
+ desc: 'line case',
+ // start with lines to lock down fix for #2888
+ patch: function(fig) {
+ fig.data.forEach(function(trace) { trace.mode = 'lines'; });
+ }
+ }, {
+ desc: 'line & markers case',
+ patch: function(fig) {
+ fig.data.forEach(function(trace) { trace.mode = 'markers+lines'; });
+ }
+ }, {
+ desc: 'gl and non-gl on same subplot case',
+ patch: function(fig) {
+ fig.data.forEach(function(trace, i) {
+ trace.type = (i % 2) ? 'scatterpolar' : 'scatterpolargl';
+ });
+ }
+ }];
+
+ specs.forEach(function(s) {
+ it('- ' + s.desc, function(done) {
+ var fig = Lib.extendDeep({}, require('@mocks/glpolar_scatter.json'));
+ scene = null;
+ gl = null;
+
+ fig.layout.hovermode = false;
+ fig.layout.width = 400;
+ fig.layout.height = 400;
+ fig.layout.margin = {l: 50, t: 50, b: 50, r: 50};
+
+ if(s.patch) s.patch(fig);
+ nTraces = fig.data
+ .filter(function(trace) { return trace.type === 'scatterpolargl'; })
+ .length;
+
+ Plotly.newPlot(gd, fig).then(function() {
+ scene = gd._fullLayout.polar._subplot._scene;
+
+ objs.forEach(function(o) {
+ if(scene[o]) {
+ spyOn(scene[o], 'draw').and.callThrough();
+ if(!gl) {
+ // all objects have the same _gl ref,
+ // spy on it just once
+ gl = scene[o].regl._gl;
+ spyOn(gl, 'clear').and.callThrough();
+ }
+ }
+ });
+ })
+ .then(function() { return _dragRadial(); })
+ .then(_assert)
+ .then(function() { return _dragAngular(); })
+ .then(_assert)
+ .catch(failTest)
+ .then(done);
+ });
+ });
+ });
});
describe('Test polar *gridshape linear* interactions', function() {
diff --git a/test/jasmine/tests/scatterpolar_test.js b/test/jasmine/tests/scatterpolar_test.js
index 58477040103..a8ee1c0945e 100644
--- a/test/jasmine/tests/scatterpolar_test.js
+++ b/test/jasmine/tests/scatterpolar_test.js
@@ -28,6 +28,10 @@ describe('Test scatterpolar trace defaults:', function() {
expect(traceOut.r).toEqual([1, 2, 3, 4, 5]);
expect(traceOut.theta).toEqual([1, 2, 3]);
expect(traceOut._length).toBe(3);
+ expect(traceOut.r0).toBeUndefined();
+ expect(traceOut.dr).toBeUndefined();
+ expect(traceOut.theta0).toBeUndefined();
+ expect(traceOut.dtheta).toBeUndefined();
});
it('should not truncate *theta* when longer than *r*', function() {
@@ -40,6 +44,39 @@ describe('Test scatterpolar trace defaults:', function() {
expect(traceOut.r).toEqual([1, 2, 3]);
expect(traceOut.theta).toEqual([1, 2, 3, 4, 5]);
expect(traceOut._length).toBe(3);
+ expect(traceOut.r0).toBeUndefined();
+ expect(traceOut.dr).toBeUndefined();
+ expect(traceOut.theta0).toBeUndefined();
+ expect(traceOut.dtheta).toBeUndefined();
+ });
+
+ it('should coerce *theta0* and *dtheta* when *theta* is not set', function() {
+ _supply({
+ r: [1, 2, 3]
+ });
+
+ expect(traceOut.r).toEqual([1, 2, 3]);
+ expect(traceOut.theta).toBeUndefined();
+ expect(traceOut._length).toBe(3);
+ expect(traceOut.r0).toBeUndefined();
+ expect(traceOut.dr).toBeUndefined();
+ expect(traceOut.theta0).toBe(0);
+ // its default value is computed later
+ expect(traceOut.dtheta).toBeUndefined();
+ });
+
+ it('should coerce *r0* and *dr* when *r* is not set', function() {
+ _supply({
+ theta: [1, 2, 3, 4, 5]
+ });
+
+ expect(traceOut.r).toBeUndefined();
+ expect(traceOut.theta).toEqual([1, 2, 3, 4, 5]);
+ expect(traceOut._length).toBe(5);
+ expect(traceOut.r0).toBe(0);
+ expect(traceOut.dr).toBe(1);
+ expect(traceOut.theta0).toBeUndefined();
+ expect(traceOut.dtheta).toBeUndefined();
});
});