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(); }); });