diff --git a/lib/barpolar.js b/lib/barpolar.js new file mode 100644 index 00000000000..720bd10a042 --- /dev/null +++ b/lib/barpolar.js @@ -0,0 +1,11 @@ +/** +* 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'; + +module.exports = require('../src/traces/barpolar'); diff --git a/lib/index.js b/lib/index.js index 2587276c897..cbb42c96bd7 100644 --- a/lib/index.js +++ b/lib/index.js @@ -53,7 +53,8 @@ Plotly.register([ require('./candlestick'), require('./scatterpolar'), - require('./scatterpolargl') + require('./scatterpolargl'), + require('./barpolar') ]); // transforms diff --git a/src/lib/angles.js b/src/lib/angles.js index b5dd27f28f9..47c42efe46d 100644 --- a/src/lib/angles.js +++ b/src/lib/angles.js @@ -8,27 +8,230 @@ 'use strict'; +var modModule = require('./mod'); +var mod = modModule.mod; +var modHalf = modModule.modHalf; + var PI = Math.PI; +var twoPI = 2 * PI; -exports.deg2rad = function(deg) { - return deg / 180 * PI; -}; +function deg2rad(deg) { return deg / 180 * PI; } -exports.rad2deg = function(rad) { - return rad / PI * 180; -}; +function rad2deg(rad) { return rad / PI * 180; } -exports.wrap360 = function(deg) { - var out = deg % 360; - return out < 0 ? out + 360 : out; -}; +/** + * is sector a full circle? + * ... this comes up a lot in SVG path-drawing routines + * + * @param {2-item array} aBnds : angular bounds in *radians* + * @return {boolean} + */ +function isFullCircle(aBnds) { + return Math.abs(Math.abs(aBnds[1] - aBnds[0]) - twoPI) < 1e-15; +} -exports.wrap180 = function(deg) { - if(Math.abs(deg) > 180) deg -= Math.round(deg / 360) * 360; - return deg; -}; +/** + * angular delta between angle 'a' and 'b' + * solution taken from: https://stackoverflow.com/a/2007279 + * + * @param {number} a : first angle in *radians* + * @param {number} b : second angle in *radians* + * @return {number} angular delta in *radians* + */ +function angleDelta(a, b) { + return modHalf(b - a, twoPI); +} + +/** + * angular distance between angle 'a' and 'b' + * + * @param {number} a : first angle in *radians* + * @param {number} b : second angle in *radians* + * @return {number} angular distance in *radians* + */ +function angleDist(a, b) { + return Math.abs(angleDelta(a, b)); +} + +/** + * is angle inside sector? + * + * @param {number} a : angle to test in *radians* + * @param {2-item array} aBnds : sector's angular bounds in *radians* + * @param {boolean} + */ +function isAngleInsideSector(a, aBnds) { + if(isFullCircle(aBnds)) return true; + + var s0, s1; + + if(aBnds[0] < aBnds[1]) { + s0 = aBnds[0]; + s1 = aBnds[1]; + } else { + s0 = aBnds[1]; + s1 = aBnds[0]; + } + + s0 = mod(s0, twoPI); + s1 = mod(s1, twoPI); + if(s0 > s1) s1 += twoPI; + + var a0 = mod(a, twoPI); + var a1 = a0 + twoPI; + + return (a0 >= s0 && a0 <= s1) || (a1 >= s0 && a1 <= s1); +} + +/** + * is pt (r,a) inside sector? + * + * @param {number} r : pt's radial coordinate + * @param {number} a : pt's angular coordinate in *radians* + * @param {2-item array} rBnds : sector's radial bounds + * @param {2-item array} aBnds : sector's angular bounds in *radians* + * @return {boolean} + */ +function isPtInsideSector(r, a, rBnds, aBnds) { + if(!isAngleInsideSector(a, aBnds)) return false; + + var r0, r1; + + if(rBnds[0] < rBnds[1]) { + r0 = rBnds[0]; + r1 = rBnds[1]; + } else { + r0 = rBnds[1]; + r1 = rBnds[0]; + } + + return r >= r0 && r <= r1; +} + +// common to pathArc, pathSector and pathAnnulus +function _path(r0, r1, a0, a1, cx, cy, isClosed) { + cx = cx || 0; + cy = cy || 0; + + var isCircle = isFullCircle([a0, a1]); + var aStart, aMid, aEnd; + var rStart, rEnd; + + if(isCircle) { + aStart = 0; + aMid = PI; + aEnd = twoPI; + } else { + if(a0 < a1) { + aStart = a0; + aEnd = a1; + } else { + aStart = a1; + aEnd = a0; + } + } + + if(r0 < r1) { + rStart = r0; + rEnd = r1; + } else { + rStart = r1; + rEnd = r0; + } + + // N.B. svg coordinates here, where y increases downward + function pt(r, a) { + return [r * Math.cos(a) + cx, cy - r * Math.sin(a)]; + } + + var largeArc = Math.abs(aEnd - aStart) <= PI ? 0 : 1; + function arc(r, a, cw) { + return 'A' + [r, r] + ' ' + [0, largeArc, cw] + ' ' + pt(r, a); + } + + var p; + + if(isCircle) { + if(rStart === null) { + p = 'M' + pt(rEnd, aStart) + + arc(rEnd, aMid, 0) + + arc(rEnd, aEnd, 0) + 'Z'; + } else { + p = 'M' + pt(rStart, aStart) + + arc(rStart, aMid, 0) + + arc(rStart, aEnd, 0) + 'Z' + + 'M' + pt(rEnd, aStart) + + arc(rEnd, aMid, 1) + + arc(rEnd, aEnd, 1) + 'Z'; + } + } else { + if(rStart === null) { + p = 'M' + pt(rEnd, aStart) + arc(rEnd, aEnd, 0); + if(isClosed) p += 'L0,0Z'; + } else { + p = 'M' + pt(rStart, aStart) + + 'L' + pt(rEnd, aStart) + + arc(rEnd, aEnd, 0) + + 'L' + pt(rStart, aEnd) + + arc(rStart, aStart, 1) + 'Z'; + } + } + + return p; +} + +/** + * path an arc + * + * @param {number} r : radius + * @param {number} a0 : first angular coordinate in *radians* + * @param {number} a1 : second angular coordinate in *radians* + * @param {number (optional)} cx : x coordinate of center + * @param {number (optional)} cy : y coordinate of center + * @return {string} svg path + */ +function pathArc(r, a0, a1, cx, cy) { + return _path(null, r, a0, a1, cx, cy, 0); +} + +/** + * path a sector + * + * @param {number} r : radius + * @param {number} a0 : first angular coordinate in *radians* + * @param {number} a1 : second angular coordinate in *radians* + * @param {number (optional)} cx : x coordinate of center + * @param {number (optional)} cy : y coordinate of center + * @return {string} svg path + */ +function pathSector(r, a0, a1, cx, cy) { + return _path(null, r, a0, a1, cx, cy, 1); +} + +/** + * path an annulus + * + * @param {number} r0 : first radial coordinate + * @param {number} r1 : second radial coordinate + * @param {number} a0 : first angular coordinate in *radians* + * @param {number} a1 : second angular coordinate in *radians* + * @param {number (optional)} cx : x coordinate of center + * @param {number (optional)} cy : y coordinate of center + * @return {string} svg path + */ +function pathAnnulus(r0, r1, a0, a1, cx, cy) { + return _path(r0, r1, a0, a1, cx, cy, 1); +} -exports.isFullCircle = function(sector) { - var arc = Math.abs(sector[1] - sector[0]); - return arc === 360; +module.exports = { + deg2rad: deg2rad, + rad2deg: rad2deg, + angleDelta: angleDelta, + angleDist: angleDist, + isFullCircle: isFullCircle, + isAngleInsideSector: isAngleInsideSector, + isPtInsideSector: isPtInsideSector, + pathArc: pathArc, + pathSector: pathSector, + pathAnnulus: pathAnnulus }; diff --git a/src/lib/coerce.js b/src/lib/coerce.js index a5d69fe22e2..421c9b4bd1c 100644 --- a/src/lib/coerce.js +++ b/src/lib/coerce.js @@ -18,7 +18,7 @@ var colorscaleNames = Object.keys(require('../components/colorscale/scales')); var nestedProperty = require('./nested_property'); var counterRegex = require('./regex').counter; var DESELECTDIM = require('../constants/interactions').DESELECTDIM; -var wrap180 = require('./angles').wrap180; +var modHalf = require('./mod').modHalf; var isArrayOrTypedArray = require('./is_array').isArrayOrTypedArray; exports.valObjectMeta = { @@ -186,7 +186,7 @@ exports.valObjectMeta = { coerceFunction: function(v, propOut, dflt) { if(v === 'auto') propOut.set('auto'); else if(!isNumeric(v)) propOut.set(dflt); - else propOut.set(wrap180(+v)); + else propOut.set(modHalf(+v, 360)); } }, subplotid: { diff --git a/src/lib/dates.js b/src/lib/dates.js index 9bfdb77616e..67828b32837 100644 --- a/src/lib/dates.js +++ b/src/lib/dates.js @@ -13,7 +13,7 @@ var d3 = require('d3'); var isNumeric = require('fast-isnumeric'); var Loggers = require('./loggers'); -var mod = require('./mod'); +var mod = require('./mod').mod; var constants = require('../constants/numerical'); var BADNUM = constants.BADNUM; diff --git a/src/lib/geometry2d.js b/src/lib/geometry2d.js index de3040cd4bb..5675609b1f1 100644 --- a/src/lib/geometry2d.js +++ b/src/lib/geometry2d.js @@ -8,7 +8,7 @@ 'use strict'; -var mod = require('./mod'); +var mod = require('./mod').mod; /* * look for intersection of two line segments diff --git a/src/lib/index.js b/src/lib/index.js index cd9f7e2ad6b..2c2177ae2b0 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -22,11 +22,14 @@ lib.nestedProperty = require('./nested_property'); lib.keyedContainer = require('./keyed_container'); lib.relativeAttr = require('./relative_attr'); lib.isPlainObject = require('./is_plain_object'); -lib.mod = require('./mod'); lib.toLogRange = require('./to_log_range'); lib.relinkPrivateKeys = require('./relink_private'); lib.ensureArray = require('./ensure_array'); +var modModule = require('./mod'); +lib.mod = modModule.mod; +lib.modHalf = modModule.modHalf; + var isArrayModule = require('./is_array'); lib.isTypedArray = isArrayModule.isTypedArray; lib.isArrayOrTypedArray = isArrayModule.isArrayOrTypedArray; @@ -62,6 +65,7 @@ lib.sorterAsc = searchModule.sorterAsc; lib.sorterDes = searchModule.sorterDes; lib.distinctVals = searchModule.distinctVals; lib.roundUp = searchModule.roundUp; +lib.findIndexOfMin = searchModule.findIndexOfMin; var statsModule = require('./stats'); lib.aggNums = statsModule.aggNums; @@ -85,9 +89,14 @@ lib.apply2DTransform2 = matrixModule.apply2DTransform2; var anglesModule = require('./angles'); lib.deg2rad = anglesModule.deg2rad; lib.rad2deg = anglesModule.rad2deg; -lib.wrap360 = anglesModule.wrap360; -lib.wrap180 = anglesModule.wrap180; +lib.angleDelta = anglesModule.angleDelta; +lib.angleDist = anglesModule.angleDist; lib.isFullCircle = anglesModule.isFullCircle; +lib.isAngleInsideSector = anglesModule.isAngleInsideSector; +lib.isPtInsideSector = anglesModule.isPtInsideSector; +lib.pathArc = anglesModule.pathArc; +lib.pathSector = anglesModule.pathSector; +lib.pathAnnulus = anglesModule.pathAnnulus; var geom2dModule = require('./geometry2d'); lib.segmentsIntersect = geom2dModule.segmentsIntersect; @@ -702,7 +711,7 @@ lib.isD3Selection = function(obj) { * * @param {d3 selection} parent : parent selection of the element in question * @param {string} nodeType : node type of element to append - * @param {string} className : class name of element in question + * @param {string} className (optional) : class name of element in question * @param {fn} enterFn (optional) : optional fn applied to entering elements only * @return {d3 selection} selection of new layer * @@ -729,7 +738,8 @@ lib.ensureSingle = function(parent, nodeType, className, enterFn) { var sel = parent.select(nodeType + (className ? '.' + className : '')); if(sel.size()) return sel; - var layer = parent.append(nodeType).classed(className, true); + var layer = parent.append(nodeType); + if(className) layer.classed(className, true); if(enterFn) layer.call(enterFn); return layer; diff --git a/src/lib/mod.js b/src/lib/mod.js index 64b1e13a49e..b391121dccd 100644 --- a/src/lib/mod.js +++ b/src/lib/mod.js @@ -12,7 +12,22 @@ * sanitized modulus function that always returns in the range [0, d) * rather than (-d, 0] if v is negative */ -module.exports = function mod(v, d) { +function mod(v, d) { var out = v % d; return out < 0 ? out + d : out; +} + +/** + * sanitized modulus function that always returns in the range [-d/2, d/2] + * rather than (-d, 0] if v is negative + */ +function modHalf(v, d) { + return Math.abs(v) > (d / 2) ? + v - Math.round(v / d) * d : + v; +} + +module.exports = { + mod: mod, + modHalf: modHalf }; diff --git a/src/lib/search.js b/src/lib/search.js index d2684f4b79e..056d707e54a 100644 --- a/src/lib/search.js +++ b/src/lib/search.js @@ -11,6 +11,7 @@ var isNumeric = require('fast-isnumeric'); var loggers = require('./loggers'); +var identity = require('./identity'); // don't trust floating point equality - fraction of bin size to call // "on the line" and ensure that they go the right way specified by @@ -113,3 +114,27 @@ exports.roundUp = function(val, arrayIn, reverse) { } return arrayIn[low]; }; + +/** + * find index in array 'arr' that minimizes 'fn' + * + * @param {array} arr : array where to search + * @param {fn (optional)} fn : function to minimize, + * if not given, fn is the identity function + * @return {integer} + */ +exports.findIndexOfMin = function(arr, fn) { + fn = fn || identity; + + var min = Infinity; + var ind; + + for(var i = 0; i < arr.length; i++) { + var v = fn(arr[i]); + if(v < min) { + min = v; + ind = i; + } + } + return ind; +}; diff --git a/src/plots/plots.js b/src/plots/plots.js index 908289fa72c..0a9296d6f38 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -2602,7 +2602,7 @@ function doCrossTraceCalc(gd) { fullLayout[sp]; for(j = 0; j < methods.length; j++) { - methods[j](gd, spInfo); + methods[j](gd, spInfo, sp); } } } diff --git a/src/plots/polar/helpers.js b/src/plots/polar/helpers.js new file mode 100644 index 00000000000..ef23e1d7cf8 --- /dev/null +++ b/src/plots/polar/helpers.js @@ -0,0 +1,293 @@ +/** +* 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 polygonTester = require('../../lib/polygon').tester; + +var findIndexOfMin = Lib.findIndexOfMin; +var isAngleInsideSector = Lib.isAngleInsideSector; +var angleDelta = Lib.angleDelta; +var angleDist = Lib.angleDist; + +/** + * is pt (r,a) inside polygon made up vertices at angles 'vangles' + * inside a given polar sector + * + * @param {number} r : pt's radial coordinate + * @param {number} a : pt's angular coordinate in *radians* + * @param {2-item array} rBnds : sector's radial bounds + * @param {2-item array} aBnds : sector's angular bounds *radians* + * @param {array} vangles : angles of polygon vertices in *radians* + * @return {boolean} + */ +function isPtInsidePolygon(r, a, rBnds, aBnds, vangles) { + if(!isAngleInsideSector(a, aBnds)) return false; + + var r0, r1; + + if(rBnds[0] < rBnds[1]) { + r0 = rBnds[0]; + r1 = rBnds[1]; + } else { + r0 = rBnds[1]; + r1 = rBnds[0]; + } + + var polygonIn = polygonTester(makePolygon(r0, aBnds[0], aBnds[1], vangles)); + var polygonOut = polygonTester(makePolygon(r1, aBnds[0], aBnds[1], vangles)); + var xy = [r * Math.cos(a), r * Math.sin(a)]; + return polygonOut.contains(xy) && !polygonIn.contains(xy); +} + +// find intersection of 'v0' <-> 'v1' edge with a ray at angle 'a' +// (i.e. a line that starts from the origin at angle 'a') +// given an (xp,yp) pair on the 'v0' <-> 'v1' line +// (N.B. 'v0' and 'v1' are angles in radians) +function findIntersectionXY(v0, v1, a, xpyp) { + var xstar, ystar; + + var xp = xpyp[0]; + var yp = xpyp[1]; + var dsin = clampTiny(Math.sin(v1) - Math.sin(v0)); + var dcos = clampTiny(Math.cos(v1) - Math.cos(v0)); + var tanA = Math.tan(a); + var cotanA = clampTiny(1 / tanA); + var m = dsin / dcos; + var b = yp - m * xp; + + if(cotanA) { + if(dsin && dcos) { + // given + // g(x) := v0 -> v1 line = m*x + b + // h(x) := ray at angle 'a' = m*x = tanA*x + // solve g(xstar) = h(xstar) + xstar = b / (tanA - m); + ystar = tanA * xstar; + } else if(dcos) { + // horizontal v0 -> v1 + xstar = yp * cotanA; + ystar = yp; + } else { + // vertical v0 -> v1 + xstar = xp; + ystar = xp * tanA; + } + } else { + // vertical ray + if(dsin && dcos) { + xstar = 0; + ystar = b; + } else if(dcos) { + xstar = 0; + ystar = yp; + } else { + // does this case exists? + xstar = ystar = NaN; + } + } + + return [xstar, ystar]; +} + +// solves l^2 = (f(x)^2 - yp)^2 + (x - xp)^2 +// rearranged into 0 = a*x^2 + b * x + c +// +// where f(x) = m*x + t + yp +// and (x0, x1) = (-b +/- del) / (2*a) +function findXYatLength(l, m, xp, yp) { + var t = -m * xp; + var a = m * m + 1; + var b = 2 * (m * t - xp); + var c = t * t + xp * xp - l * l; + var del = Math.sqrt(b * b - 4 * a * c); + var x0 = (-b + del) / (2 * a); + var x1 = (-b - del) / (2 * a); + return [ + [x0, m * x0 + t + yp], + [x1, m * x1 + t + yp] + ]; +} + +function makeRegularPolygon(r, vangles) { + var len = vangles.length; + var vertices = new Array(len + 1); + var i; + for(i = 0; i < len; i++) { + var va = vangles[i]; + vertices[i] = [r * Math.cos(va), r * Math.sin(va)]; + } + vertices[i] = vertices[0].slice(); + return vertices; +} + +function makeClippedPolygon(r, a0, a1, vangles) { + var len = vangles.length; + var vertices = []; + var i, j; + + function a2xy(a) { + return [r * Math.cos(a), r * Math.sin(a)]; + } + + function findXY(va0, va1, s) { + return findIntersectionXY(va0, va1, s, a2xy(va0)); + } + + function cycleIndex(ind) { + return Lib.mod(ind, len); + } + + function isInside(v) { + return isAngleInsideSector(v, [a0, a1]); + } + + // find index in sector closest to a0 + // use it to find intersection of v[i0] <-> v[i0-1] edge with sector radius + var i0 = findIndexOfMin(vangles, function(v) { + return isInside(v) ? angleDist(v, a0) : Infinity; + }); + var xy0 = findXY(vangles[i0], vangles[cycleIndex(i0 - 1)], a0); + vertices.push(xy0); + + // fill in in-sector vertices + for(i = i0, j = 0; j < len; i++, j++) { + var va = vangles[cycleIndex(i)]; + if(!isInside(va)) break; + vertices.push(a2xy(va)); + } + + // find index in sector closest to a1, + // use it to find intersection of v[iN] <-> v[iN+1] edge with sector radius + var iN = findIndexOfMin(vangles, function(v) { + return isInside(v) ? angleDist(v, a1) : Infinity; + }); + var xyN = findXY(vangles[iN], vangles[cycleIndex(iN + 1)], a1); + vertices.push(xyN); + + vertices.push([0, 0]); + vertices.push(vertices[0].slice()); + + return vertices; +} + +function makePolygon(r, a0, a1, vangles) { + return Lib.isFullCircle([a0, a1]) ? + makeRegularPolygon(r, vangles) : + makeClippedPolygon(r, a0, a1, vangles); +} + +function findPolygonOffset(r, a0, a1, vangles) { + var minX = Infinity; + var minY = Infinity; + var vertices = makePolygon(r, a0, a1, vangles); + + for(var i = 0; i < vertices.length; i++) { + var v = vertices[i]; + minX = Math.min(minX, v[0]); + minY = Math.min(minY, -v[1]); + } + return [minX, minY]; +} + +/** + * find vertex angles (in 'vangles') the enclose angle 'a' + * + * @param {number} a : angle in *radians* + * @param {array} vangles : angles of polygon vertices in *radians* + * @return {2-item array} + */ +function findEnclosingVertexAngles(a, vangles) { + var minFn = function(v) { + var adelta = angleDelta(v, a); + return adelta > 0 ? adelta : Infinity; + }; + var i0 = findIndexOfMin(vangles, minFn); + var i1 = Lib.mod(i0 + 1, vangles.length); + return [vangles[i0], vangles[i1]]; +} + +// to more easily catch 'almost zero' numbers in if-else blocks +function clampTiny(v) { + return Math.abs(v) > 1e-10 ? v : 0; +} + +function transformForSVG(pts0, cx, cy) { + cx = cx || 0; + cy = cy || 0; + + var len = pts0.length; + var pts1 = new Array(len); + + for(var i = 0; i < len; i++) { + var pt = pts0[i]; + pts1[i] = [cx + pt[0], cy - pt[1]]; + } + return pts1; +} + +/** + * path polygon + * + * @param {number} r : polygon 'radius' + * @param {number} a0 : first angular coordinate in *radians* + * @param {number} a1 : second angular coordinate in *radians* + * @param {array} vangles : angles of polygon vertices in *radians* + * @param {number (optional)} cx : x coordinate of center + * @param {number (optional)} cy : y coordinate of center + * @return {string} svg path + * + */ +function pathPolygon(r, a0, a1, vangles, cx, cy) { + var poly = makePolygon(r, a0, a1, vangles); + return 'M' + transformForSVG(poly, cx, cy).join('L'); +} + +/** + * path a polygon 'annulus' + * i.e. a polygon with a concentric hole + * + * N.B. this routine uses the evenodd SVG rule + * + * @param {number} r0 : first radial coordinate + * @param {number} r1 : second radial coordinate + * @param {number} a0 : first angular coordinate in *radians* + * @param {number} a1 : second angular coordinate in *radians* + * @param {array} vangles : angles of polygon vertices in *radians* + * @param {number (optional)} cx : x coordinate of center + * @param {number (optional)} cy : y coordinate of center + * @return {string} svg path + * + */ +function pathPolygonAnnulus(r0, r1, a0, a1, vangles, cx, cy) { + var rStart, rEnd; + + if(r0 < r1) { + rStart = r0; + rEnd = r1; + } else { + rStart = r1; + rEnd = r0; + } + + var inner = transformForSVG(makePolygon(rStart, a0, a1, vangles), cx, cy); + var outer = transformForSVG(makePolygon(rEnd, a0, a1, vangles), cx, cy); + return 'M' + outer.reverse().join('L') + 'M' + inner.join('L'); +} + +module.exports = { + isPtInsidePolygon: isPtInsidePolygon, + findPolygonOffset: findPolygonOffset, + findEnclosingVertexAngles: findEnclosingVertexAngles, + findIntersectionXY: findIntersectionXY, + findXYatLength: findXYatLength, + clampTiny: clampTiny, + pathPolygon: pathPolygon, + pathPolygonAnnulus: pathPolygonAnnulus +}; diff --git a/src/plots/polar/polar.js b/src/plots/polar/polar.js index 500659f55db..e550b52b325 100644 --- a/src/plots/polar/polar.js +++ b/src/plots/polar/polar.js @@ -27,17 +27,15 @@ var Titles = require('../../components/titles'); var prepSelect = require('../cartesian/select').prepSelect; var clearSelect = require('../cartesian/select').clearSelect; var setCursor = require('../../lib/setcursor'); -var polygonTester = require('../../lib/polygon').tester; var MID_SHIFT = require('../../constants/alignment').MID_SHIFT; var constants = require('./constants'); +var helpers = require('./helpers'); var _ = Lib._; +var mod = Lib.mod; var deg2rad = Lib.deg2rad; var rad2deg = Lib.rad2deg; -var wrap360 = Lib.wrap360; -var wrap180 = Lib.wrap180; -var isFullCircle = Lib.isFullCircle; function Polar(gd, id) { this.id = id; @@ -91,7 +89,7 @@ proto.plot = function(polarCalcData, fullLayout) { _this.updateLayers(fullLayout, polarLayout); _this.updateLayout(fullLayout, polarLayout); Plots.generalUpdatePerTraceModule(_this.gd, _this, polarCalcData, polarLayout); - _this.updateFx(fullLayout, polarLayout); + _this.updateFx(fullLayout); }; proto.updateLayers = function(fullLayout, polarLayout) { @@ -129,6 +127,8 @@ proto.updateLayers = function(fullLayout, polarLayout) { switch(d) { case 'frontplot': sel.append('g').classed('scatterlayer', true); + // TODO add option to place in 'backplot' layer?? + sel.append('g').classed('barlayer', true); break; case 'backplot': sel.append('g').classed('maplayer', true); @@ -200,7 +200,8 @@ proto.updateLayout = function(fullLayout, polarLayout) { var xLength = _this.xLength = gs.w * (xDomain[1] - xDomain[0]); var yLength = _this.yLength = gs.h * (yDomain[1] - yDomain[0]); // sector to plot - var sector = _this.sector = polarLayout.sector; + var sector = polarLayout.sector; + _this.sectorInRad = sector.map(deg2rad); var sectorBBox = _this.sectorBBox = computeSectorBBox(sector); var dxSectorBBox = sectorBBox[2] - sectorBBox[0]; var dySectorBBox = sectorBBox[3] - sectorBBox[1]; @@ -271,32 +272,20 @@ proto.updateLayout = function(fullLayout, polarLayout) { _this.updateRadialAxis(fullLayout, polarLayout); _this.updateRadialAxisTitle(fullLayout, polarLayout); - var radialRange = _this.radialAxis.range; - var rSpan = radialRange[1] - radialRange[0]; - - var xaxis = _this.xaxis = { - type: 'linear', + _this.xaxis = _this.mockCartesianAxis(fullLayout, polarLayout, { _id: 'x', - range: [sectorBBox[0] * rSpan, sectorBBox[2] * rSpan], domain: xDomain2 - }; - setConvertCartesian(xaxis, fullLayout); - xaxis.setScale(); + }); - var yaxis = _this.yaxis = { - type: 'linear', + _this.yaxis = _this.mockCartesianAxis(fullLayout, polarLayout, { _id: 'y', - range: [sectorBBox[1] * rSpan, sectorBBox[3] * rSpan], domain: yDomain2 - }; - setConvertCartesian(yaxis, fullLayout); - yaxis.setScale(); + }); - xaxis.isPtWithinRange = function(d) { return _this.isPtWithinSector(d); }; - yaxis.isPtWithinRange = function() { return true; }; + var dPath = _this.pathSector(); _this.clipPaths.forTraces.select('path') - .attr('d', pathSectorClosed(radius, sector, _this.vangles)) + .attr('d', dPath) .attr('transform', strTranslate(cxx, cyy)); layers.frontplot @@ -304,7 +293,7 @@ proto.updateLayout = function(fullLayout, polarLayout) { .call(Drawing.setClipUrl, _this._hasClipOnAxisFalse ? null : _this.clipIds.forTraces); layers.bg - .attr('d', pathSectorClosed(radius, sector, _this.vangles)) + .attr('d', dPath) .attr('transform', strTranslate(cx, cy)) .call(Color.fill, polarLayout.bgcolor); @@ -330,6 +319,35 @@ proto.mockAxis = function(fullLayout, polarLayout, axLayout, opts) { return ax; }; +proto.mockCartesianAxis = function(fullLayout, polarLayout, opts) { + var _this = this; + var axId = opts._id; + + var ax = Lib.extendFlat({type: 'linear'}, opts); + setConvertCartesian(ax, fullLayout); + + var bboxIndices = { + x: [0, 2], + y: [1, 3] + }; + + ax.setRange = function() { + var sectorBBox = _this.sectorBBox; + var rl = _this.radialAxis._rl; + var drl = rl[1] - rl[0]; + var ind = bboxIndices[axId]; + ax.range = [sectorBBox[ind[0]] * drl, sectorBBox[ind[1]] * drl]; + }; + + ax.isPtWithinRange = axId === 'x' ? + function(d) { return _this.isPtInside(d); } : + function() { return true; }; + + ax.setRange(); + ax.setScale(); + return ax; +}; + proto.doAutoRange = function(fullLayout, polarLayout) { var gd = this.gd; var radialAxis = this.radialAxis; @@ -341,6 +359,11 @@ proto.doAutoRange = function(fullLayout, polarLayout) { var rng = radialAxis.range; radialLayout.range = rng.slice(); radialLayout._input.range = rng.slice(); + + radialAxis._rl = [ + radialAxis.r2l(rng[0], null, 'gregorian'), + radialAxis.r2l(rng[1], null, 'gregorian') + ]; }; proto.updateRadialAxis = function(fullLayout, polarLayout) { @@ -351,8 +374,7 @@ proto.updateRadialAxis = function(fullLayout, polarLayout) { var cx = _this.cx; var cy = _this.cy; var radialLayout = polarLayout.radialaxis; - var sector = polarLayout.sector; - var a0 = wrap360(sector[0]); + var a0 = mod(polarLayout.sector[0], 360); var ax = _this.radialAxis; _this.fillViewInitialKey('radialaxis.angle', radialLayout.angle); @@ -375,8 +397,7 @@ proto.updateRadialAxis = function(fullLayout, polarLayout) { // set special grid path function ax._gridpath = function(d) { - var r = ax.r2p(d.x); - return pathSector(r, sector, _this.vangles); + return _this.pathArc(ax.r2p(d.x)); }; var newTickLayout = strTickLayout(radialLayout); @@ -461,7 +482,6 @@ proto.updateAngularAxis = function(fullLayout, polarLayout) { var cx = _this.cx; var cy = _this.cy; var angularLayout = polarLayout.angularaxis; - var sector = polarLayout.sector; var ax = _this.angularAxis; _this.fillViewInitialKey('angularaxis.rotation', angularLayout.rotation); @@ -486,7 +506,9 @@ proto.updateAngularAxis = function(fullLayout, polarLayout) { // the range w.r.t sector, so that sectors that cross 360 can // show all their ticks. if(ax.type === 'category') { - ax._tickFilter = function(d) { return isAngleInSector(t2g(d), sector); }; + ax._tickFilter = function(d) { + return Lib.isAngleInsideSector(t2g(d), _this.sectorInRad); + }; } ax._transfn = function(d) { @@ -560,7 +582,7 @@ proto.updateAngularAxis = function(fullLayout, polarLayout) { // ax._vals should be always ordered, make them // always turn counterclockwise for convenience here - if(angleDelta(vangles[0], vangles[1]) < 0) { + if(Lib.angleDelta(vangles[0], vangles[1]) < 0) { vangles = vangles.slice().reverse(); } } else { @@ -569,22 +591,22 @@ proto.updateAngularAxis = function(fullLayout, polarLayout) { _this.vangles = vangles; updateElement(layers['angular-line'].select('path'), angularLayout.showline, { - d: pathSectorClosed(radius, sector, vangles), + d: _this.pathSector(), transform: strTranslate(cx, cy) }) .attr('stroke-width', angularLayout.linewidth) .call(Color.stroke, angularLayout.linecolor); }; -proto.updateFx = function(fullLayout, polarLayout) { +proto.updateFx = function(fullLayout) { if(!this.gd._context.staticPlot) { - this.updateAngularDrag(fullLayout, polarLayout); - this.updateRadialDrag(fullLayout, polarLayout); - this.updateMainDrag(fullLayout, polarLayout); + this.updateAngularDrag(fullLayout); + this.updateRadialDrag(fullLayout); + this.updateMainDrag(fullLayout); } }; -proto.updateMainDrag = function(fullLayout, polarLayout) { +proto.updateMainDrag = function(fullLayout) { var _this = this; var gd = _this.gd; var layers = _this.layers; @@ -596,15 +618,18 @@ proto.updateMainDrag = function(fullLayout, polarLayout) { var cy = _this.cy; var cxx = _this.cxx; var cyy = _this.cyy; - var sector = polarLayout.sector; + var sectorInRad = _this.sectorInRad; var vangles = _this.vangles; + var clampTiny = helpers.clampTiny; + var findXYatLength = helpers.findXYatLength; + var findEnclosingVertexAngles = helpers.findEnclosingVertexAngles; var chw = constants.cornerHalfWidth; var chl = constants.cornerLen / 2; var mainDrag = dragBox.makeDragger(layers, 'path', 'maindrag', 'crosshair'); d3.select(mainDrag) - .attr('d', pathSectorClosed(radius, sector, vangles)) + .attr('d', _this.pathSector()) .attr('transform', strTranslate(cx, cy)); var dragOpts = { @@ -644,12 +669,8 @@ proto.updateMainDrag = function(fullLayout, polarLayout) { return [r * Math.cos(a), r * Math.sin(-a)]; } - function _pathSectorClosed(r) { - return pathSectorClosed(r, sector, vangles); - } - function pathCorner(r, a) { - if(r === 0) return _pathSectorClosed(2 * chw); + if(r === 0) return _this.pathSector(2 * chw); var da = chl / r; var am = a - da; @@ -670,7 +691,7 @@ proto.updateMainDrag = function(fullLayout, polarLayout) { // ... we could eventually add another mode for cursor // angles 'close to' enough to a particular vertex. function pathCornerForPolygons(r, va0, va1) { - if(r === 0) return _pathSectorClosed(2 * chw); + if(r === 0) return _this.pathSector(2 * chw); var xy0 = ra2xy(r, va0); var xy1 = ra2xy(r, va1); @@ -706,7 +727,7 @@ proto.updateMainDrag = function(fullLayout, polarLayout) { function zoomPrep() { r0 = null; r1 = null; - path0 = _pathSectorClosed(radius); + path0 = _this.pathSector(); dimmed = false; var polarLayoutNow = gd._fullLayout[_this.id]; @@ -768,24 +789,15 @@ proto.updateMainDrag = function(fullLayout, polarLayout) { var cpath; if(clampAndSetR0R1(rr0, rr1)) { - path1 = path0 + _pathSectorClosed(r1) + _pathSectorClosed(r0); + path1 = path0 + _this.pathSector(r1) + _this.pathSector(r0); // keep 'starting' angle cpath = pathCorner(r0, a0) + pathCorner(r1, a0); } applyZoomMove(path1, cpath); } - function findEnclosingVertexAngles(a) { - var i0 = findIndexOfMin(vangles, function(v) { - var adelta = angleDelta(v, a); - return adelta > 0 ? adelta : Infinity; - }); - var i1 = Lib.mod(i0 + 1, vangles.length); - return [vangles[i0], vangles[i1]]; - } - function findPolygonRadius(x, y, va0, va1) { - var xy = findIntersectionXY(va0, va1, va0, [x - cxx, cyy - y]); + var xy = helpers.findIntersectionXY(va0, va1, va0, [x - cxx, cyy - y]); return norm(xy[0], xy[1]); } @@ -794,15 +806,15 @@ proto.updateMainDrag = function(fullLayout, polarLayout) { var y1 = y0 + dy; var a0 = xy2a(x0, y0); var a1 = xy2a(x1, y1); - var vangles0 = findEnclosingVertexAngles(a0); - var vangles1 = findEnclosingVertexAngles(a1); + var vangles0 = findEnclosingVertexAngles(a0, vangles); + var vangles1 = findEnclosingVertexAngles(a1, vangles); var rr0 = findPolygonRadius(x0, y0, vangles0[0], vangles0[1]); var rr1 = Math.min(findPolygonRadius(x1, y1, vangles1[0], vangles1[1]), radius); var path1; var cpath; if(clampAndSetR0R1(rr0, rr1)) { - path1 = path0 + _pathSectorClosed(r1) + _pathSectorClosed(r0); + path1 = path0 + _this.pathSector(r1) + _this.pathSector(r0); // keep 'starting' angle here too cpath = [ pathCornerForPolygons(r0, vangles0[0], vangles0[1]), @@ -820,12 +832,12 @@ proto.updateMainDrag = function(fullLayout, polarLayout) { dragBox.showDoubleClickNotifier(gd); var radialAxis = _this.radialAxis; - var radialRange = radialAxis.range; - var drange = radialRange[1] - radialRange[0]; + var rl = radialAxis._rl; + var drl = rl[1] - rl[0]; var updateObj = {}; updateObj[_this.id + '.radialaxis.range'] = [ - radialRange[0] + r0 * drange / radius, - radialRange[0] + r1 * drange / radius + rl[0] + r0 * drl / radius, + rl[0] + r1 * drl / radius ]; Registry.call('relayout', gd, updateObj); @@ -841,7 +853,7 @@ proto.updateMainDrag = function(fullLayout, polarLayout) { // need to offset x/y as bbox center does not // match origin for asymmetric polygons if(vangles) { - var offset = findPolygonOffset(radius, sector, vangles); + var offset = helpers.findPolygonOffset(radius, sectorInRad[0], sectorInRad[1], vangles); x0 += cxx + offset[0]; y0 += cyy + offset[1]; } @@ -894,7 +906,7 @@ proto.updateMainDrag = function(fullLayout, polarLayout) { dragElement.init(dragOpts); }; -proto.updateRadialDrag = function(fullLayout, polarLayout) { +proto.updateRadialDrag = function(fullLayout) { var _this = this; var gd = _this.gd; var layers = _this.layers; @@ -902,15 +914,16 @@ proto.updateRadialDrag = function(fullLayout, polarLayout) { var cx = _this.cx; var cy = _this.cy; var radialAxis = _this.radialAxis; - var radialLayout = polarLayout.radialaxis; + + if(!radialAxis.visible) return; + var angle0 = deg2rad(_this.radialAxisAngle); - var range0 = radialAxis.range.slice(); - var drange = range0[1] - range0[0]; + var rl0 = radialAxis._rl[0]; + var rl1 = radialAxis._rl[1]; + var drl = rl1 - rl0; + var bl = constants.radialDragBoxSize; var bl2 = bl / 2; - - if(!radialLayout.visible) return; - var radialDrag = dragBox.makeRectDragger(layers, 'radialdrag', 'crosshair', -bl2, -bl2, bl, bl); var dragOpts = {element: radialDrag, gd: gd}; var tx = cx + (radius + bl2) * Math.cos(angle0); @@ -970,24 +983,27 @@ proto.updateRadialDrag = function(fullLayout, polarLayout) { function rerangeMove(dx, dy) { // project (dx, dy) unto unit radial axis vector var dr = Lib.dot([dx, -dy], [Math.cos(angle0), Math.sin(angle0)]); - var rprime = range0[1] - drange * dr / radius * 0.75; + rng1 = rl1 - drl * dr / radius * 0.75; // make sure new range[1] does not change the range[0] -> range[1] sign - if((drange > 0) !== (rprime > range0[0])) return; - rng1 = radialAxis.range[1] = rprime; + if((drl > 0) !== (rng1 > rl0)) return; - doTicksSingle(gd, _this.radialAxis, true); - layers['radial-grid'] - .attr('transform', strTranslate(cx, cy)) - .selectAll('path').attr('transform', null); + // update radial range -> update c2g -> update _m,_b + radialAxis.range[1] = rng1; + radialAxis._rl[1] = rng1; + radialAxis.setGeometry(); + radialAxis.setScale(); - var rSpan = rng1 - range0[0]; - var sectorBBox = _this.sectorBBox; - _this.xaxis.range = [sectorBBox[0] * rSpan, sectorBBox[2] * rSpan]; - _this.yaxis.range = [sectorBBox[1] * rSpan, sectorBBox[3] * rSpan]; + _this.xaxis.setRange(); _this.xaxis.setScale(); + _this.yaxis.setRange(); _this.yaxis.setScale(); + doTicksSingle(gd, radialAxis, true); + layers['radial-grid'] + .attr('transform', strTranslate(cx, cy)) + .selectAll('path').attr('transform', null); + if(_this._scene) _this._scene.clear(); for(var traceType in _this.traceHash) { @@ -995,14 +1011,7 @@ proto.updateRadialDrag = function(fullLayout, polarLayout) { 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(traceType, 'gl')) { - for(var i = 0; i < moduleCalcDataVisible.length; i++) { - _module.style(gd, moduleCalcDataVisible[i]); - } - } } } @@ -1028,7 +1037,7 @@ proto.updateRadialDrag = function(fullLayout, polarLayout) { dragElement.init(dragOpts); }; -proto.updateAngularDrag = function(fullLayout, polarLayout) { +proto.updateAngularDrag = function(fullLayout) { var _this = this; var gd = _this.gd; var layers = _this.layers; @@ -1038,24 +1047,13 @@ proto.updateAngularDrag = function(fullLayout, polarLayout) { var cy = _this.cy; var cxx = _this.cxx; var cyy = _this.cyy; - var sector = polarLayout.sector; var dbs = constants.angularDragBoxSize; var angularDrag = dragBox.makeDragger(layers, 'path', 'angulardrag', 'move'); var dragOpts = {element: angularDrag, gd: gd}; - var angularDragPath; - - if(_this.vangles) { - // use evenodd svg rule - var outer = invertY(makePolygon(radius + dbs, sector, _this.vangles)); - var inner = invertY(makePolygon(radius, sector, _this.vangles)); - angularDragPath = 'M' + outer.reverse().join('L') + 'M' + inner.join('L'); - } else { - angularDragPath = pathAnnulus(radius, radius + dbs, sector); - } d3.select(angularDrag) - .attr('d', angularDragPath) + .attr('d', _this.pathAnnulus(radius, radius + dbs)) .attr('transform', strTranslate(cx, cy)) .call(setCursor, 'move'); @@ -1123,13 +1121,13 @@ proto.updateAngularDrag = function(fullLayout, polarLayout) { }); // update rotation -> range -> _m,_b - angularAxis.rotation = wrap180(rot1); + angularAxis.rotation = Lib.modHalf(rot1, 360); angularAxis.setGeometry(); angularAxis.setScale(); doTicksSingle(gd, angularAxis, true); - if(_this._hasClipOnAxisFalse && !isFullCircle(sector)) { + if(_this._hasClipOnAxisFalse && !Lib.isFullCircle(_this.sectorInRad)) { scatterTraces.call(Drawing.hideOutsideRangePoints, _this); } @@ -1174,7 +1172,7 @@ proto.updateAngularDrag = function(fullLayout, polarLayout) { }; // I don't what we should do in this case, skip we now - if(_this.vangles && !isFullCircle(sector)) { + if(_this.vangles && !Lib.isFullCircle(_this.sectorInRad)) { dragOpts.prepFn = Lib.noop; setCursor(d3.select(angularDrag), null); } @@ -1182,36 +1180,39 @@ proto.updateAngularDrag = function(fullLayout, polarLayout) { dragElement.init(dragOpts); }; -proto.isPtWithinSector = function(d) { - var sector = this.sector; +proto.isPtInside = function(d) { + var sectorInRad = this.sectorInRad; + var vangles = this.vangles; var thetag = this.angularAxis.c2g(d.theta); + var radialAxis = this.radialAxis; + var r = radialAxis.c2l(d.r); + var rl = radialAxis._rl; - if(!isAngleInSector(thetag, sector)) { - return false; - } + var fn = vangles ? helpers.isPtInsidePolygon : Lib.isPtInsideSector; + return fn(r, thetag, rl, sectorInRad, vangles); +}; +proto.pathArc = function(r) { + r = r || this.radius; + var sectorInRad = this.sectorInRad; var vangles = this.vangles; - var radialAxis = this.radialAxis; - var radialRange = radialAxis.range; - var r = radialAxis.c2r(d.r); - - var r0, r1; - if(radialRange[1] >= radialRange[0]) { - r0 = radialRange[0]; - r1 = radialRange[1]; - } else { - r0 = radialRange[1]; - r1 = radialRange[0]; - } + var fn = vangles ? helpers.pathPolygon : Lib.pathArc; + return fn(r, sectorInRad[0], sectorInRad[1], vangles); +}; - if(vangles) { - var polygonIn = polygonTester(makePolygon(r0, sector, vangles)); - var polygonOut = polygonTester(makePolygon(r1, sector, vangles)); - var xy = [r * Math.cos(thetag), r * Math.sin(thetag)]; - return polygonOut.contains(xy) && !polygonIn.contains(xy); - } +proto.pathSector = function(r) { + r = r || this.radius; + var sectorInRad = this.sectorInRad; + var vangles = this.vangles; + var fn = vangles ? helpers.pathPolygon : Lib.pathSector; + return fn(r, sectorInRad[0], sectorInRad[1], vangles); +}; - return r >= r0 && r <= r1; +proto.pathAnnulus = function(r0, r1) { + var sectorInRad = this.sectorInRad; + var vangles = this.vangles; + var fn = vangles ? helpers.pathPolygonAnnulus : Lib.pathAnnulus; + return fn(r0, r1, sectorInRad[0], sectorInRad[1], vangles); }; proto.fillViewInitialKey = function(key, val) { @@ -1230,13 +1231,13 @@ function strTickLayout(axLayout) { // inspired by https://math.stackexchange.com/q/1852703 // // assumes: -// - sector[1] < sector[0] +// - sector[0] < sector[1] // - counterclockwise rotation function computeSectorBBox(sector) { var s0 = sector[0]; var s1 = sector[1]; var arc = s1 - s0; - var a0 = wrap360(s0); + var a0 = mod(s0, 360); var a1 = a0 + arc; var ax0 = Math.cos(deg2rad(a0)); @@ -1281,277 +1282,12 @@ function computeSectorBBox(sector) { return [x0, y0, x1, y1]; } -function isAngleInSector(rad, sector) { - if(isFullCircle(sector)) return true; - - var s0 = wrap360(sector[0]); - var s1 = wrap360(sector[1]); - if(s0 > s1) s1 += 360; - - var deg = wrap360(rad2deg(rad)); - var nextTurnDeg = deg + 360; - - return (deg >= s0 && deg <= s1) || - (nextTurnDeg >= s0 && nextTurnDeg <= s1); -} - function snapToVertexAngle(a, vangles) { - function angleDeltaAbs(va) { - return Math.abs(angleDelta(a, va)); - } - - var ind = findIndexOfMin(vangles, angleDeltaAbs); + var fn = function(v) { return Lib.angleDist(a, v); }; + var ind = Lib.findIndexOfMin(vangles, fn); return vangles[ind]; } -// taken from https://stackoverflow.com/a/2007279 -function angleDelta(a, b) { - var d = b - a; - return Math.atan2(Math.sin(d), Math.cos(d)); -} - -function findIndexOfMin(arr, fn) { - fn = fn || Lib.identity; - - var min = Infinity; - var ind; - - for(var i = 0; i < arr.length; i++) { - var v = fn(arr[i]); - if(v < min) { - min = v; - ind = i; - } - } - return ind; -} - -// find intersection of 'v0' <-> 'v1' edge with a ray at angle 'a' -// (i.e. a line that starts from the origin at angle 'a') -// given an (xp,yp) pair on the 'v0' <-> 'v1' line -// (N.B. 'v0' and 'v1' are angles in radians) -function findIntersectionXY(v0, v1, a, xpyp) { - var xstar, ystar; - - var xp = xpyp[0]; - var yp = xpyp[1]; - var dsin = clampTiny(Math.sin(v1) - Math.sin(v0)); - var dcos = clampTiny(Math.cos(v1) - Math.cos(v0)); - var tanA = Math.tan(a); - var cotanA = clampTiny(1 / tanA); - var m = dsin / dcos; - var b = yp - m * xp; - - if(cotanA) { - if(dsin && dcos) { - // given - // g(x) := v0 -> v1 line = m*x + b - // h(x) := ray at angle 'a' = m*x = tanA*x - // solve g(xstar) = h(xstar) - xstar = b / (tanA - m); - ystar = tanA * xstar; - } else if(dcos) { - // horizontal v0 -> v1 - xstar = yp * cotanA; - ystar = yp; - } else { - // vertical v0 -> v1 - xstar = xp; - ystar = xp * tanA; - } - } else { - // vertical ray - if(dsin && dcos) { - xstar = 0; - ystar = b; - } else if(dcos) { - xstar = 0; - ystar = yp; - } else { - // does this case exists? - xstar = ystar = NaN; - } - } - - return [xstar, ystar]; -} - -// solves l^2 = (f(x)^2 - yp)^2 + (x - xp)^2 -// rearranged into 0 = a*x^2 + b * x + c -// -// where f(x) = m*x + t + yp -// and (x0, x1) = (-b +/- del) / (2*a) -function findXYatLength(l, m, xp, yp) { - var t = -m * xp; - var a = m * m + 1; - var b = 2 * (m * t - xp); - var c = t * t + xp * xp - l * l; - var del = Math.sqrt(b * b - 4 * a * c); - var x0 = (-b + del) / (2 * a); - var x1 = (-b - del) / (2 * a); - return [ - [x0, m * x0 + t + yp], - [x1, m * x1 + t + yp] - ]; -} - -function makeRegularPolygon(r, vangles) { - var len = vangles.length; - var vertices = new Array(len + 1); - var i; - for(i = 0; i < len; i++) { - var va = vangles[i]; - vertices[i] = [r * Math.cos(va), r * Math.sin(va)]; - } - vertices[i] = vertices[0].slice(); - return vertices; -} - -function makeClippedPolygon(r, sector, vangles) { - var len = vangles.length; - var vertices = []; - var i, j; - - function a2xy(a) { - return [r * Math.cos(a), r * Math.sin(a)]; - } - - function findXY(va0, va1, s) { - return findIntersectionXY(va0, va1, s, a2xy(va0)); - } - - function cycleIndex(ind) { - return Lib.mod(ind, len); - } - - var s0 = deg2rad(sector[0]); - var s1 = deg2rad(sector[1]); - - // find index in sector closest to sector[0], - // use it to find intersection of v[i0] <-> v[i0-1] edge with sector radius - var i0 = findIndexOfMin(vangles, function(v) { - return isAngleInSector(v, sector) ? Math.abs(angleDelta(v, s0)) : Infinity; - }); - var xy0 = findXY(vangles[i0], vangles[cycleIndex(i0 - 1)], s0); - vertices.push(xy0); - - // fill in in-sector vertices - for(i = i0, j = 0; j < len; i++, j++) { - var va = vangles[cycleIndex(i)]; - if(!isAngleInSector(va, sector)) break; - vertices.push(a2xy(va)); - } - - // find index in sector closest to sector[1], - // use it to find intersection of v[iN] <-> v[iN+1] edge with sector radius - var iN = findIndexOfMin(vangles, function(v) { - return isAngleInSector(v, sector) ? Math.abs(angleDelta(v, s1)) : Infinity; - }); - var xyN = findXY(vangles[iN], vangles[cycleIndex(iN + 1)], s1); - vertices.push(xyN); - - vertices.push([0, 0]); - vertices.push(vertices[0].slice()); - - return vertices; -} - -function makePolygon(r, sector, vangles) { - return isFullCircle(sector) ? - makeRegularPolygon(r, vangles) : - makeClippedPolygon(r, sector, vangles); -} - -function findPolygonOffset(r, sector, vangles) { - var minX = Infinity; - var minY = Infinity; - var vertices = makePolygon(r, sector, vangles); - - for(var i = 0; i < vertices.length; i++) { - var v = vertices[i]; - minX = Math.min(minX, v[0]); - minY = Math.min(minY, -v[1]); - } - return [minX, minY]; -} - -function invertY(pts0) { - var len = pts0.length; - var pts1 = new Array(len); - for(var i = 0; i < len; i++) { - var pt = pts0[i]; - pts1[i] = [pt[0], -pt[1]]; - } - return pts1; -} - -function pathSector(r, sector, vangles) { - var d; - - if(vangles) { - d = 'M' + invertY(makePolygon(r, sector, vangles)).join('L'); - } else if(isFullCircle(sector)) { - d = Drawing.symbolFuncs[0](r); - } else { - var arc = Math.abs(sector[1] - sector[0]); - var flags = arc <= 180 ? [0, 0, 0] : [0, 1, 0]; - var xs = r * Math.cos(deg2rad(sector[0])); - var ys = -r * Math.sin(deg2rad(sector[0])); - var xe = r * Math.cos(deg2rad(sector[1])); - var ye = -r * Math.sin(deg2rad(sector[1])); - - d = 'M' + [xs, ys] + - 'A' + [r, r] + ' ' + flags + ' ' + [xe, ye]; - } - - return d; -} - -function pathSectorClosed(r, sector, vangles) { - var d = pathSector(r, sector, vangles); - if(isFullCircle(sector) || vangles) return d; - return d + 'L0,0Z'; -} - -// TODO recycle this routine with the ones used for pie traces. -function pathAnnulus(r0, r1, sector) { - var largeArc = Math.abs(sector[1] - sector[0]) <= 180 ? 0 : 1; - // sector angle at [s]tart, [m]iddle and [e]nd - var ss, sm, se; - - function pt(r, s) { - return [r * Math.cos(s), -r * Math.sin(s)]; - } - - function arc(r, s, cw) { - return 'A' + [r, r] + ' ' + [0, largeArc, cw] + ' ' + pt(r, s); - } - - if(isFullCircle(sector)) { - ss = 0; - se = 2 * Math.PI; - sm = Math.PI; - return 'M' + pt(r0, ss) + - arc(r0, sm, 0) + - arc(r0, se, 0) + - 'Z' + - 'M' + pt(r1, ss) + - arc(r1, sm, 1) + - arc(r1, se, 1) + - 'Z'; - } else { - ss = deg2rad(sector[0]); - se = deg2rad(sector[1]); - return 'M' + pt(r0, ss) + - 'L' + pt(r1, ss) + - arc(r1, se, 0) + - 'L' + pt(r0, se) + - arc(r0, ss, 1) + - 'Z'; - } -} - - function updateElement(sel, showAttr, attrs) { if(showAttr) { sel.attr('display', null); @@ -1570,11 +1306,6 @@ function strRotate(angle) { return 'rotate(' + angle + ')'; } -// to more easily catch 'almost zero' numbers in if-else blocks -function clampTiny(v) { - return Math.abs(v) > 1e-10 ? v : 0; -} - // because Math.sign(Math.cos(Math.PI / 2)) === 1 // oh javascript ;) function sign(v) { diff --git a/src/plots/polar/set_convert.js b/src/plots/polar/set_convert.js index d7b12f5d679..7eb8af9b852 100644 --- a/src/plots/polar/set_convert.js +++ b/src/plots/polar/set_convert.js @@ -13,7 +13,6 @@ var setConvertCartesian = require('../cartesian/set_convert'); var deg2rad = Lib.deg2rad; var rad2deg = Lib.rad2deg; -var isFullCircle = Lib.isFullCircle; /** * setConvert for polar axes! @@ -53,7 +52,7 @@ module.exports = function setConvert(ax, polarLayout, fullLayout) { switch(ax._id) { case 'x': case 'radialaxis': - setConvertRadial(ax); + setConvertRadial(ax, polarLayout); break; case 'angularaxis': setConvertAngular(ax, polarLayout); @@ -61,22 +60,28 @@ module.exports = function setConvert(ax, polarLayout, fullLayout) { } }; -function setConvertRadial(ax) { +function setConvertRadial(ax, polarLayout) { ax.setGeometry = function() { - var rng = ax.range; + var rl0 = ax._rl[0]; + var rl1 = ax._rl[1]; - var rFilter = rng[0] > rng[1] ? + var rFilter = rl0 > rl1 ? function(v) { return v <= 0; } : function(v) { return v >= 0; }; ax.c2g = function(v) { - var r = ax.c2r(v) - rng[0]; + var r = ax.c2l(v) - rl0; return rFilter(r) ? r : 0; }; ax.g2c = function(v) { - return ax.r2c(v + rng[0]); + return ax.l2c(v + rl0); }; + + var m = polarLayout._subplot.radius / (rl1 - rl0); + + ax.g2p = function(v) { return v * m; }; + ax.c2p = function(v) { return ax.g2p(ax.c2g(v)); }; }; } @@ -138,6 +143,7 @@ function setConvertAngular(ax, polarLayout) { // N.B. we mock the axis 'range' here ax.setGeometry = function() { var sector = polarLayout.sector; + var sectorInRad = sector.map(deg2rad); var dir = {clockwise: -1, counterclockwise: 1}[ax.direction]; var rot = deg2rad(ax.rotation); @@ -155,9 +161,9 @@ function setConvertAngular(ax, polarLayout) { // 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) ? + ax.range = Lib.isFullCircle(sectorInRad) ? sector.slice() : - sector.map(deg2rad).map(g2rad).map(rad2deg); + sectorInRad.map(g2rad).map(rad2deg); break; case 'category': diff --git a/src/traces/bar/cross_trace_calc.js b/src/traces/bar/cross_trace_calc.js index e00e9e8779f..4aa2ba3e562 100644 --- a/src/traces/bar/cross_trace_calc.js +++ b/src/traces/bar/cross_trace_calc.js @@ -24,7 +24,7 @@ var Sieve = require('./sieve.js'); * now doing this one subplot at a time */ -module.exports = function crossTraceCalc(gd, plotinfo) { +function crossTraceCalc(gd, plotinfo) { var xa = plotinfo.xaxis, ya = plotinfo.yaxis; @@ -52,8 +52,7 @@ module.exports = function crossTraceCalc(gd, plotinfo) { setGroupPositions(gd, xa, ya, calcTracesVertical); setGroupPositions(gd, ya, xa, calcTracesHorizontal); -}; - +} function setGroupPositions(gd, pa, sa, calcTraces) { if(!calcTraces.length) return; @@ -248,7 +247,7 @@ function setGroupPositionsInStackOrRelativeMode(gd, pa, sa, calcTraces) { function setOffsetAndWidth(gd, pa, sieve) { var fullLayout = gd._fullLayout, bargap = fullLayout.bargap, - bargroupgap = fullLayout.bargroupgap, + bargroupgap = fullLayout.bargroupgap || 0, minDiff = sieve.minDiff, calcTraces = sieve.traces, i, calcTrace, calcTrace0, @@ -291,7 +290,7 @@ function setOffsetAndWidth(gd, pa, sieve) { function setOffsetAndWidthInGroupMode(gd, pa, sieve) { var fullLayout = gd._fullLayout, bargap = fullLayout.bargap, - bargroupgap = fullLayout.bargroupgap, + bargroupgap = fullLayout.bargroupgap || 0, positions = sieve.positions, distinctPositions = sieve.distinctPositions, minDiff = sieve.minDiff, @@ -351,7 +350,7 @@ function applyAttributes(sieve) { fullTrace = calcTrace0.trace; t = calcTrace0.t; - var offset = fullTrace.offset, + var offset = fullTrace._offset || fullTrace.offset, initialPoffset = t.poffset, newPoffset; @@ -378,7 +377,7 @@ function applyAttributes(sieve) { t.poffset = offset; } - var width = fullTrace.width, + var width = fullTrace._width || fullTrace.width, initialBarwidth = t.barwidth; if(isArrayOrTypedArray(width)) { @@ -684,22 +683,37 @@ function collectExtents(calcTraces, pa) { return String(Math.round(roundFactor * (p - pMin))); }; + var poffset, poffsetIsArray; + for(i = 0; i < calcTraces.length; i++) { cd = calcTraces[i]; cd[0].t.extents = extents; + poffset = cd[0].t.poffset; + poffsetIsArray = Array.isArray(poffset); + for(j = 0; j < cd.length; j++) { var di = cd[j]; var p0 = di[posLetter] - di.w / 2; + if(isNumeric(p0)) { var p1 = di[posLetter] + di.w / 2; var pVal = round(di.p); if(extents[pVal]) { extents[pVal] = [Math.min(p0, extents[pVal][0]), Math.max(p1, extents[pVal][1])]; - } - else { + } else { extents[pVal] = [p0, p1]; } } + + di.p0 = di.p + ((poffsetIsArray) ? poffset[j] : poffset); + di.p1 = di.p0 + di.w; + di.s0 = di.b; + di.s1 = di.s0 + di.s; } } } + +module.exports = { + crossTraceCalc: crossTraceCalc, + setGroupPositions: setGroupPositions +}; diff --git a/src/traces/bar/hover.js b/src/traces/bar/hover.js index 933b4401232..5e5b5d9f9ec 100644 --- a/src/traces/bar/hover.js +++ b/src/traces/bar/hover.js @@ -14,7 +14,7 @@ var Registry = require('../../registry'); var Color = require('../../components/color'); var fillHoverText = require('../scatter/fill_hover_text'); -module.exports = function hoverPoints(pointData, xval, yval, hovermode) { +function hoverPoints(pointData, xval, yval, hovermode) { var cd = pointData.cd; var trace = cd[0].trace; var t = cd[0].t; @@ -117,12 +117,6 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { // the closest data point var index = pointData.index; var di = cd[index]; - var mc = di.mcc || trace.marker.color; - var mlc = di.mlcc || trace.marker.line.color; - var mlw = di.mlw || trace.marker.line.width; - - if(Color.opacity(mc)) pointData.color = mc; - else if(Color.opacity(mlc) && mlw) pointData.color = mlc; var size = (trace.base) ? di.b + di.s : di.s; pointData[sizeLetter + '0'] = pointData[sizeLetter + '1'] = sa.c2p(di[sizeLetter], true); @@ -139,8 +133,23 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { // in case of bars shifted within groups pointData[posLetter + 'Spike'] = pa.c2p(di.p, true); + pointData.color = getTraceColor(trace, di); fillHoverText(di, trace, pointData); Registry.getComponentMethod('errorbars', 'hoverInfo')(di, trace, pointData); return [pointData]; +} + +function getTraceColor(trace, di) { + var mc = di.mcc || trace.marker.color; + var mlc = di.mlcc || trace.marker.line.color; + var mlw = di.mlw || trace.marker.line.width; + + if(Color.opacity(mc)) return mc; + else if(Color.opacity(mlc) && mlw) return mlc; +} + +module.exports = { + hoverPoints: hoverPoints, + getTraceColor: getTraceColor }; diff --git a/src/traces/bar/index.js b/src/traces/bar/index.js index 91836d265c6..4ed81f6bb6f 100644 --- a/src/traces/bar/index.js +++ b/src/traces/bar/index.js @@ -16,13 +16,13 @@ Bar.layoutAttributes = require('./layout_attributes'); Bar.supplyDefaults = require('./defaults'); Bar.supplyLayoutDefaults = require('./layout_defaults'); Bar.calc = require('./calc'); -Bar.crossTraceCalc = require('./cross_trace_calc'); +Bar.crossTraceCalc = require('./cross_trace_calc').crossTraceCalc; Bar.colorbar = require('../scatter/marker_colorbar'); Bar.arraysToCalcdata = require('./arrays_to_calcdata'); Bar.plot = require('./plot'); Bar.style = require('./style').style; Bar.styleOnSelect = require('./style').styleOnSelect; -Bar.hoverPoints = require('./hover'); +Bar.hoverPoints = require('./hover').hoverPoints; Bar.selectPoints = require('./select'); Bar.moduleType = 'trace'; diff --git a/src/traces/bar/plot.js b/src/traces/bar/plot.js index 433b30d628d..ead3f4eb754 100644 --- a/src/traces/bar/plot.js +++ b/src/traces/bar/plot.js @@ -38,14 +38,10 @@ module.exports = function plot(gd, plotinfo, cdbar, barLayer) { var bartraces = Lib.makeTraceGroups(barLayer, cdbar, 'trace bars').each(function(cd) { var plotGroup = d3.select(this); var cd0 = cd[0]; - var t = cd0.t; var trace = cd0.trace; if(!plotinfo.isRangePlot) cd0.node3 = plotGroup; - var poffset = t.poffset; - var poffsetIsArray = Array.isArray(poffset); - var pointGroup = Lib.ensureSingle(plotGroup, 'g', 'points'); var bars = pointGroup.selectAll('g.point').data(Lib.identity); @@ -62,26 +58,21 @@ module.exports = function plot(gd, plotinfo, cdbar, barLayer) { // clipped xf/yf (2nd arg true): non-positive // log values go off-screen by plotwidth // so you see them continue if you drag the plot - var p0 = di.p + ((poffsetIsArray) ? poffset[i] : poffset), - p1 = p0 + di.w, - s0 = di.b, - s1 = s0 + di.s; - var x0, x1, y0, y1; if(trace.orientation === 'h') { - y0 = ya.c2p(p0, true); - y1 = ya.c2p(p1, true); - x0 = xa.c2p(s0, true); - x1 = xa.c2p(s1, true); + y0 = ya.c2p(di.p0, true); + y1 = ya.c2p(di.p1, true); + x0 = xa.c2p(di.s0, true); + x1 = xa.c2p(di.s1, true); // for selections di.ct = [x1, (y0 + y1) / 2]; } else { - x0 = xa.c2p(p0, true); - x1 = xa.c2p(p1, true); - y0 = ya.c2p(s0, true); - y1 = ya.c2p(s1, true); + x0 = xa.c2p(di.p0, true); + x1 = xa.c2p(di.p1, true); + y0 = ya.c2p(di.s0, true); + y1 = ya.c2p(di.s1, true); // for selections di.ct = [(x0 + x1) / 2, y1]; diff --git a/src/traces/barpolar/attributes.js b/src/traces/barpolar/attributes.js new file mode 100644 index 00000000000..ed0cb373476 --- /dev/null +++ b/src/traces/barpolar/attributes.js @@ -0,0 +1,78 @@ +/** +* 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 extendFlat = require('../../lib/extend').extendFlat; +var scatterPolarAttrs = require('../scatterpolar/attributes'); +var barAttrs = require('../bar/attributes'); + +module.exports = { + r: scatterPolarAttrs.r, + theta: scatterPolarAttrs.theta, + r0: scatterPolarAttrs.r0, + dr: scatterPolarAttrs.dr, + theta0: scatterPolarAttrs.theta0, + dtheta: scatterPolarAttrs.dtheta, + thetaunit: scatterPolarAttrs.thetaunit, + + // orientation: { + // valType: 'enumerated', + // role: 'info', + // values: ['radial', 'angular'], + // editType: 'calc+clearAxisTypes', + // description: 'Sets the orientation of the bars.' + // }, + + base: extendFlat({}, barAttrs.base, { + description: [ + 'Sets where the bar base is drawn (in radial axis units).', + 'In *stack* barmode,', + 'traces that set *base* will be excluded', + 'and drawn in *overlay* mode instead.' + ].join(' ') + }), + offset: extendFlat({}, barAttrs.offset, { + description: [ + 'Shifts the angular position where the bar is drawn', + '(in *thetatunit* units).' + ].join(' ') + }), + width: extendFlat({}, barAttrs.width, { + description: [ + 'Sets the bar angular width (in *thetaunit* units).' + ].join(' ') + }), + + text: extendFlat({}, barAttrs.text, { + description: [ + 'Sets hover text elements associated with each bar.', + 'If a single string, the same string appears over all bars.', + 'If an array of string, the items are mapped in order to the', + 'this trace\'s coordinates.' + ].join(' ') + }), + // hovertext: barAttrs.hovertext, + + // textposition: {}, + // textfont: {}, + // insidetextfont: {}, + // outsidetextfont: {}, + // constraintext: {}, + // cliponaxis: extendFlat({}, barAttrs.cliponaxis, {dflt: false}), + + marker: barAttrs.marker, + + hoverinfo: scatterPolarAttrs.hoverinfo, + + selected: barAttrs.selected, + unselected: barAttrs.unselected + + // error_x (error_r, error_theta) + // error_y +}; diff --git a/src/traces/barpolar/calc.js b/src/traces/barpolar/calc.js new file mode 100644 index 00000000000..4c9876ef5c2 --- /dev/null +++ b/src/traces/barpolar/calc.js @@ -0,0 +1,101 @@ +/** +* 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 hasColorscale = require('../../components/colorscale/has_colorscale'); +var colorscaleCalc = require('../../components/colorscale/calc'); +var arraysToCalcdata = require('../bar/arrays_to_calcdata'); +var setGroupPositions = require('../bar/cross_trace_calc').setGroupPositions; +var calcSelection = require('../scatter/calc_selection'); +var traceIs = require('../../registry').traceIs; +var extendFlat = require('../../lib').extendFlat; + +function calc(gd, trace) { + var fullLayout = gd._fullLayout; + var subplotId = trace.subplot; + var radialAxis = fullLayout[subplotId].radialaxis; + var angularAxis = fullLayout[subplotId].angularaxis; + var rArray = radialAxis.makeCalcdata(trace, 'r'); + var thetaArray = angularAxis.makeCalcdata(trace, 'theta'); + var len = trace._length; + var cd = new Array(len); + + // 'size' axis variables + var sArray = rArray; + // 'pos' axis variables + var pArray = thetaArray; + + for(var i = 0; i < len; i++) { + cd[i] = {p: pArray[i], s: sArray[i]}; + } + + // convert width and offset in 'c' coordinate, + // set 'c' value(s) in trace._width and trace._offset, + // to make Bar.crossTraceCalc "just work" + function d2c(attr) { + var val = trace[attr]; + if(val !== undefined) { + trace['_' + attr] = Array.isArray(val) ? + angularAxis.makeCalcdata(trace, attr) : + angularAxis.d2c(val, trace.thetaunit); + } + } + + if(angularAxis.type === 'linear') { + d2c('width'); + d2c('offset'); + } + + if(hasColorscale(trace, 'marker')) { + colorscaleCalc(trace, trace.marker.color, 'marker', 'c'); + } + if(hasColorscale(trace, 'marker.line')) { + colorscaleCalc(trace, trace.marker.line.color, 'marker.line', 'c'); + } + + arraysToCalcdata(cd, trace); + calcSelection(cd, trace); + + return cd; +} + +function crossTraceCalc(gd, polarLayout, subplotId) { + var calcdata = gd.calcdata; + var barPolarCd = []; + + for(var i = 0; i < calcdata.length; i++) { + var cdi = calcdata[i]; + var trace = cdi[0].trace; + + if(trace.visible === true && traceIs(trace, 'bar') && + trace.subplot === subplotId + ) { + barPolarCd.push(cdi); + } + } + + // to make _extremes is filled in correctly so that + // polar._subplot.radialAxis can get auotrange'd + // TODO clean up! + // I think we want to call getAutorange on polar.radialaxis + // NOT on polar._subplot.radialAxis + var rAxis = extendFlat({}, polarLayout.radialaxis, {_id: 'x'}); + var aAxis = polarLayout.angularaxis; + + // 'bargap', 'barmode' are in _fullLayout.polar + // TODO clean up setGroupPositions API instead + var mockGd = {_fullLayout: polarLayout}; + + setGroupPositions(mockGd, aAxis, rAxis, barPolarCd); +} + +module.exports = { + calc: calc, + crossTraceCalc: crossTraceCalc +}; diff --git a/src/traces/barpolar/defaults.js b/src/traces/barpolar/defaults.js new file mode 100644 index 00000000000..eeb63fe5e0d --- /dev/null +++ b/src/traces/barpolar/defaults.js @@ -0,0 +1,56 @@ +/** +* 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 handleRThetaDefaults = require('../scatterpolar/defaults').handleRThetaDefaults; +var handleStyleDefaults = require('../bar/style_defaults'); +var attributes = require('./attributes'); + +module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); + } + + var len = handleRThetaDefaults(traceIn, traceOut, layout, coerce); + if(!len) { + traceOut.visible = false; + return; + } + + // coerce('orientation', (traceOut.theta && !traceOut.r) ? 'angular' : 'radial'); + + coerce('thetaunit'); + coerce('base'); + coerce('offset'); + coerce('width'); + + coerce('text'); + // coerce('hovertext'); + + // var textPosition = coerce('textposition'); + // var hasBoth = Array.isArray(textPosition) || textPosition === 'auto'; + // var hasInside = hasBoth || textPosition === 'inside'; + // var hasOutside = hasBoth || textPosition === 'outside'; + + // if(hasInside || hasOutside) { + // var textFont = coerceFont(coerce, 'textfont', layout.font); + // if(hasInside) coerceFont(coerce, 'insidetextfont', textFont); + // if(hasOutside) coerceFont(coerce, 'outsidetextfont', textFont); + // coerce('constraintext'); + // coerce('selected.textfont.color'); + // coerce('unselected.textfont.color'); + // coerce('cliponaxis'); + // } + + handleStyleDefaults(traceIn, traceOut, coerce, defaultColor, layout); + + Lib.coerceSelectionMarkerOpacity(traceOut, coerce); +}; diff --git a/src/traces/barpolar/hover.js b/src/traces/barpolar/hover.js new file mode 100644 index 00000000000..be292f2eb66 --- /dev/null +++ b/src/traces/barpolar/hover.js @@ -0,0 +1,70 @@ +/** +* 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 Fx = require('../../components/fx'); +var Lib = require('../../lib'); +var getTraceColor = require('../bar/hover').getTraceColor; +var makeHoverPointText = require('../scatterpolar/hover').makeHoverPointText; +var isPtInsidePolygon = require('../../plots/polar/helpers').isPtInsidePolygon; + +module.exports = function hoverPoints(pointData, xval, yval) { + var cd = pointData.cd; + var trace = cd[0].trace; + + var subplot = pointData.subplot; + var radialAxis = subplot.radialAxis; + var angularAxis = subplot.angularAxis; + var vangles = subplot.vangles; + var inboxFn = vangles ? isPtInsidePolygon : Lib.isPtInsideSector; + var maxHoverDistance = pointData.maxHoverDistance; + var period = angularAxis._period || 2 * Math.PI; + + var rVal = Math.abs(radialAxis.g2p(Math.sqrt(xval * xval + yval * yval))); + var thetaVal = Math.atan2(yval, xval); + + // polar.(x|y)axis.p2c doesn't get the reversed radial axis range case right + if(radialAxis.range[0] > radialAxis.range[1]) { + thetaVal += Math.PI; + } + + var distFn = function(di) { + if(inboxFn(rVal, thetaVal, [di.rp0, di.rp1], [di.thetag0, di.thetag1], vangles)) { + return maxHoverDistance + + // add a little to the pseudo-distance for wider bars, so that like scatter, + // if you are over two overlapping bars, the narrower one wins. + Math.min(1, Math.abs(di.thetag1 - di.thetag0) / period) - 1 + + // add a gradient so hovering near the end of a + // bar makes it a little closer match + (di.rp1 - rVal) / (di.rp1 - di.rp0) - 1; + } else { + return Infinity; + } + }; + + Fx.getClosest(cd, distFn, pointData); + if(pointData.index === false) return; + + var index = pointData.index; + var cdi = cd[index]; + + pointData.x0 = pointData.x1 = cdi.ct[0]; + pointData.y0 = pointData.y1 = cdi.ct[1]; + + var _cdi = Lib.extendFlat({}, cdi, {r: cdi.s, theta: cdi.p}); + pointData.extraText = makeHoverPointText(_cdi, trace, subplot); + pointData.color = getTraceColor(trace, cdi); + pointData.xLabelVal = pointData.yLabelVal = undefined; + + if(cdi.s < 0) { + pointData.idealAlign = 'left'; + } + + return [pointData]; +}; diff --git a/src/traces/barpolar/index.js b/src/traces/barpolar/index.js new file mode 100644 index 00000000000..7511e066c4f --- /dev/null +++ b/src/traces/barpolar/index.js @@ -0,0 +1,41 @@ +/** +* 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'; + +module.exports = { + moduleType: 'trace', + name: 'barpolar', + basePlotModule: require('../../plots/polar'), + categories: ['polar', 'bar', 'showLegend'], + + attributes: require('./attributes'), + layoutAttributes: require('./layout_attributes'), + supplyDefaults: require('./defaults'), + supplyLayoutDefaults: require('./layout_defaults'), + + calc: require('./calc').calc, + crossTraceCalc: require('./calc').crossTraceCalc, + + plot: require('./plot'), + colorbar: require('../scatter/marker_colorbar'), + style: require('../bar/style').style, + + hoverPoints: require('./hover'), + selectPoints: require('../bar/select'), + + meta: { + hrName: 'bar_polar', + description: [ + 'The data visualized by the radial span of the bars is set in `r`' + // 'if `orientation` is set th *radial* (the default)', + // 'and the labels are set in `theta`.', + // 'By setting `orientation` to *angular*, the roles are interchanged.' + ].join(' ') + } +}; diff --git a/src/traces/barpolar/layout_attributes.js b/src/traces/barpolar/layout_attributes.js new file mode 100644 index 00000000000..0d8c05fded6 --- /dev/null +++ b/src/traces/barpolar/layout_attributes.js @@ -0,0 +1,40 @@ +/** +* 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'; + +module.exports = { + barmode: { + valType: 'enumerated', + values: ['stack', 'overlay'], + dflt: 'stack', + role: 'info', + editType: 'calc', + description: [ + 'Determines how bars at the same location coordinate', + 'are displayed on the graph.', + 'With *stack*, the bars are stacked on top of one another', + 'With *overlay*, the bars are plotted over one another,', + 'you might need to an *opacity* to see multiple bars.' + ].join(' ') + }, + bargap: { + valType: 'number', + dflt: 0.1, + min: 0, + max: 1, + role: 'style', + editType: 'calc', + description: [ + 'Sets the gap between bars of', + 'adjacent location coordinates.', + 'Values are unitless, they represent fractions of the minimum difference', + 'in bar positions in the data.' + ].join(' ') + } +}; diff --git a/src/traces/barpolar/layout_defaults.js b/src/traces/barpolar/layout_defaults.js new file mode 100644 index 00000000000..0a2f011f2f8 --- /dev/null +++ b/src/traces/barpolar/layout_defaults.js @@ -0,0 +1,27 @@ +/** +* 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 attrs = require('./layout_attributes'); + +module.exports = function(layoutIn, layoutOut) { + var subplotIds = layoutOut._subplots.polar; + var sp; + + function coerce(attr, dflt) { + return Lib.coerce(layoutIn[sp] || {}, layoutOut[sp], attrs, attr, dflt); + } + + for(var i = 0; i < subplotIds.length; i++) { + sp = subplotIds[i]; + coerce('barmode'); + coerce('bargap'); + } +}; diff --git a/src/traces/barpolar/plot.js b/src/traces/barpolar/plot.js new file mode 100644 index 00000000000..30bba248fd5 --- /dev/null +++ b/src/traces/barpolar/plot.js @@ -0,0 +1,102 @@ +/** +* 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 d3 = require('d3'); +var isNumeric = require('fast-isnumeric'); + +var Lib = require('../../lib'); +var Drawing = require('../../components/drawing'); +var helpers = require('../../plots/polar/helpers'); + +module.exports = function plot(gd, subplot, cdbar) { + var xa = subplot.xaxis; + var ya = subplot.yaxis; + var radialAxis = subplot.radialAxis; + var angularAxis = subplot.angularAxis; + var pathFn = makePathFn(subplot); + var barLayer = subplot.layers.frontplot.select('g.barlayer'); + + Lib.makeTraceGroups(barLayer, cdbar, 'trace bars').each(function(cd) { + var plotGroup = cd[0].node3 = d3.select(this); + var pointGroup = Lib.ensureSingle(plotGroup, 'g', 'points'); + var bars = pointGroup.selectAll('g.point').data(Lib.identity); + + bars.enter().append('g') + .style('vector-effect', 'non-scaling-stroke') + .style('stroke-miterlimit', 2) + .classed('point', true); + + bars.exit().remove(); + + bars.each(function(di) { + var bar = d3.select(this); + + var rp0 = di.rp0 = radialAxis.c2p(di.s0); + var rp1 = di.rp1 = radialAxis.c2p(di.s1); + var thetag0 = di.thetag0 = angularAxis.c2g(di.p0); + var thetag1 = di.thetag1 = angularAxis.c2g(di.p1); + + var dPath; + + if(!isNumeric(rp0) || !isNumeric(rp1) || + !isNumeric(thetag0) || !isNumeric(thetag1) || + rp0 === rp1 || thetag0 === thetag1 + ) { + // do not remove blank bars, to keep data-to-node + // mapping intact during radial drag, that we + // can skip calling _module.style during interactions + dPath = 'M0,0Z'; + } else { + // this 'center' pt is used for selections and hover labels + var rg1 = radialAxis.c2g(di.s1); + var thetagMid = (thetag0 + thetag1) / 2; + di.ct = [ + xa.c2p(rg1 * Math.cos(thetagMid)), + ya.c2p(rg1 * Math.sin(thetagMid)) + ]; + + dPath = pathFn(rp0, rp1, thetag0, thetag1); + } + + Lib.ensureSingle(bar, 'path').attr('d', dPath); + }); + + // clip plotGroup, when trace layer isn't clipped + Drawing.setClipUrl(plotGroup, subplot._hasClipOnAxisFalse ? subplot.clipIds.forTraces : null); + }); +}; + +function makePathFn(subplot) { + var cxx = subplot.cxx; + var cyy = subplot.cyy; + + if(subplot.vangles) { + return function(r0, r1, _a0, _a1) { + var a0, a1; + + if(Lib.angleDelta(_a0, _a1) > 0) { + a0 = _a0; + a1 = _a1; + } else { + a0 = _a1; + a1 = _a0; + } + + var va0 = helpers.findEnclosingVertexAngles(a0, subplot.vangles)[0]; + var va1 = helpers.findEnclosingVertexAngles(a1, subplot.vangles)[1]; + var vaBar = [va0, (a0 + a1) / 2, va1]; + return helpers.pathPolygonAnnulus(r0, r1, a0, a1, vaBar, cxx, cyy); + }; + } + + return function(r0, r1, a0, a1) { + return Lib.pathAnnulus(r0, r1, a0, a1, cxx, cyy); + }; +} diff --git a/src/traces/histogram/calc.js b/src/traces/histogram/calc.js index 4cdb97514bf..7d4659719c2 100644 --- a/src/traces/histogram/calc.js +++ b/src/traces/histogram/calc.js @@ -176,15 +176,17 @@ module.exports = function calc(gd, trace) { b: 0 }; - // pts and p0/p1 don't seem to make much sense for cumulative distributions + // setup hover and event data fields, + // N.B. pts and "hover" positions ph0/ph1 don't seem to make much sense + // for cumulative distributions if(!cumulativeSpec.enabled) { cdi.pts = inputPoints[i]; if(uniqueValsPerBin) { - cdi.p0 = cdi.p1 = (inputPoints[i].length) ? pos0[inputPoints[i][0]] : pos[i]; + cdi.ph0 = cdi.ph1 = (inputPoints[i].length) ? pos0[inputPoints[i][0]] : pos[i]; } else { - cdi.p0 = roundFn(binEdges[i]); - cdi.p1 = roundFn(binEdges[i + 1], true); + cdi.ph0 = roundFn(binEdges[i]); + cdi.ph1 = roundFn(binEdges[i + 1], true); } } cd.push(cdi); diff --git a/src/traces/histogram/hover.js b/src/traces/histogram/hover.js index 84c7e2ee49f..cf79f533d6a 100644 --- a/src/traces/histogram/hover.js +++ b/src/traces/histogram/hover.js @@ -9,7 +9,7 @@ 'use strict'; -var barHover = require('../bar/hover'); +var barHover = require('../bar/hover').hoverPoints; var hoverLabelText = require('../../plots/cartesian/axes').hoverLabelText; module.exports = function hoverPoints(pointData, xval, yval, hovermode) { @@ -24,7 +24,7 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { if(!trace.cumulative.enabled) { var posLetter = trace.orientation === 'h' ? 'y' : 'x'; - pointData[posLetter + 'Label'] = hoverLabelText(pointData[posLetter + 'a'], di.p0, di.p1); + pointData[posLetter + 'Label'] = hoverLabelText(pointData[posLetter + 'a'], di.ph0, di.ph1); } return pts; diff --git a/src/traces/histogram/index.js b/src/traces/histogram/index.js index 50abc6f24b7..25fc27a227e 100644 --- a/src/traces/histogram/index.js +++ b/src/traces/histogram/index.js @@ -30,7 +30,7 @@ Histogram.layoutAttributes = require('../bar/layout_attributes'); Histogram.supplyDefaults = require('./defaults'); Histogram.supplyLayoutDefaults = require('../bar/layout_defaults'); Histogram.calc = require('./calc'); -Histogram.crossTraceCalc = require('../bar/cross_trace_calc'); +Histogram.crossTraceCalc = require('../bar/cross_trace_calc').crossTraceCalc; Histogram.plot = require('../bar/plot'); Histogram.layerName = 'barlayer'; Histogram.style = require('../bar/style').style; diff --git a/src/traces/scattermapbox/hover.js b/src/traces/scattermapbox/hover.js index 0f21d133aae..020bce29abf 100644 --- a/src/traces/scattermapbox/hover.js +++ b/src/traces/scattermapbox/hover.js @@ -35,7 +35,7 @@ module.exports = function hoverPoints(pointData, xval, yval) { var lonlat = d.lonlat; if(lonlat[0] === BADNUM) return Infinity; - var lon = Lib.wrap180(lonlat[0]); + var lon = Lib.modHalf(lonlat[0], 360); var lat = lonlat[1]; var pt = subplot.project([lon, lat]); var dx = pt.x - xa.c2p([xval2, lat]); @@ -52,7 +52,7 @@ module.exports = function hoverPoints(pointData, xval, yval) { var di = cd[pointData.index]; var lonlat = di.lonlat; - var lonlatShifted = [Lib.wrap180(lonlat[0]) + lonShift, lonlat[1]]; + var lonlatShifted = [Lib.modHalf(lonlat[0], 360) + lonShift, lonlat[1]]; // shift labels back to original winded globe var xc = xa.c2p(lonlatShifted); diff --git a/src/traces/scattermapbox/select.js b/src/traces/scattermapbox/select.js index ade2ea5ceeb..dd6f3536903 100644 --- a/src/traces/scattermapbox/select.js +++ b/src/traces/scattermapbox/select.js @@ -32,7 +32,7 @@ module.exports = function selectPoints(searchInfo, polygon) { var lonlat = di.lonlat; if(lonlat[0] !== BADNUM) { - var lonlat2 = [Lib.wrap180(lonlat[0]), lonlat[1]]; + var lonlat2 = [Lib.modHalf(lonlat[0], 360), lonlat[1]]; var xy = [xa.c2p(lonlat2), ya.c2p(lonlat2)]; if(polygon.contains(xy)) { diff --git a/src/traces/scatterpolar/hover.js b/src/traces/scatterpolar/hover.js index 6eaad2950c0..e9dc9563c99 100644 --- a/src/traces/scatterpolar/hover.js +++ b/src/traces/scatterpolar/hover.js @@ -27,7 +27,7 @@ function hoverPoints(pointData, xval, yval, hovermode) { var cdi = newPointData.cd[newPointData.index]; var trace = newPointData.trace; - if(!subplot.isPtWithinSector(cdi)) return; + if(!subplot.isPtInside(cdi)) return; newPointData.xLabelVal = undefined; newPointData.yLabelVal = undefined; @@ -52,7 +52,7 @@ function makeHoverPointText(cdi, trace, subplot) { if(parts.indexOf('all') !== -1) parts = ['r', 'theta']; if(parts.indexOf('r') !== -1) { - textPart(radialAxis, radialAxis.c2r(cdi.r)); + textPart(radialAxis, radialAxis.c2l(cdi.r)); } if(parts.indexOf('theta') !== -1) { var theta = cdi.theta; diff --git a/src/traces/scatterpolargl/index.js b/src/traces/scatterpolargl/index.js index b2e6be07249..1e7f58d93a9 100644 --- a/src/traces/scatterpolargl/index.js +++ b/src/traces/scatterpolargl/index.js @@ -61,7 +61,7 @@ function plot(container, subplot, cdata) { // filter out by range for(i = 0; i < rArray.length; i++) { - if(!subplot.isPtWithinSector({r: rArray[i], theta: thetaArray[i]})) { + if(!subplot.isPtInside({r: rArray[i], theta: thetaArray[i]})) { subRArray[i] = NaN; subThetaArray[i] = NaN; } @@ -161,7 +161,7 @@ function hoverPoints(pointData, xval, yval, hovermode) { cdi.r = rArray[newPointData.index]; cdi.theta = thetaArray[newPointData.index]; - if(!subplot.isPtWithinSector(cdi)) return; + if(!subplot.isPtInside(cdi)) return; newPointData.xLabelVal = undefined; newPointData.yLabelVal = undefined; diff --git a/test/image/baselines/polar_bar-overlay.png b/test/image/baselines/polar_bar-overlay.png new file mode 100644 index 00000000000..54e29302826 Binary files /dev/null and b/test/image/baselines/polar_bar-overlay.png differ diff --git a/test/image/baselines/polar_bar-stacked.png b/test/image/baselines/polar_bar-stacked.png new file mode 100644 index 00000000000..c232d2b3c0a Binary files /dev/null and b/test/image/baselines/polar_bar-stacked.png differ diff --git a/test/image/baselines/polar_bar-width-base-offset.png b/test/image/baselines/polar_bar-width-base-offset.png new file mode 100644 index 00000000000..760c06c857c Binary files /dev/null and b/test/image/baselines/polar_bar-width-base-offset.png differ diff --git a/test/image/baselines/polar_dates.png b/test/image/baselines/polar_dates.png index 5ae17bc9f23..6127f9afd71 100644 Binary files a/test/image/baselines/polar_dates.png and b/test/image/baselines/polar_dates.png differ diff --git a/test/image/baselines/polar_funky-bars.png b/test/image/baselines/polar_funky-bars.png new file mode 100644 index 00000000000..721da8c124b Binary files /dev/null and b/test/image/baselines/polar_funky-bars.png differ diff --git a/test/image/baselines/polar_polygon-bars.png b/test/image/baselines/polar_polygon-bars.png new file mode 100644 index 00000000000..a6dabee586c Binary files /dev/null and b/test/image/baselines/polar_polygon-bars.png differ diff --git a/test/image/baselines/polar_r0dr-theta0dtheta.png b/test/image/baselines/polar_r0dr-theta0dtheta.png index 4345d2092ee..e5b78152117 100644 Binary files a/test/image/baselines/polar_r0dr-theta0dtheta.png and b/test/image/baselines/polar_r0dr-theta0dtheta.png differ diff --git a/test/image/baselines/polar_radial-range.png b/test/image/baselines/polar_radial-range.png index 56e6f4a853c..e79a88f3b3d 100644 Binary files a/test/image/baselines/polar_radial-range.png and b/test/image/baselines/polar_radial-range.png differ diff --git a/test/image/baselines/polar_transforms.png b/test/image/baselines/polar_transforms.png index 8cebb23abcd..c7c0a5eb218 100644 Binary files a/test/image/baselines/polar_transforms.png and b/test/image/baselines/polar_transforms.png differ diff --git a/test/image/baselines/polar_wind-rose.png b/test/image/baselines/polar_wind-rose.png new file mode 100644 index 00000000000..d1050119a3f Binary files /dev/null and b/test/image/baselines/polar_wind-rose.png differ diff --git a/test/image/mocks/polar_bar-overlay.json b/test/image/mocks/polar_bar-overlay.json new file mode 100644 index 00000000000..16b504fd5b1 --- /dev/null +++ b/test/image/mocks/polar_bar-overlay.json @@ -0,0 +1,21 @@ +{ + "data": [{ + "type": "barpolar", + "r": [1, 2, 3], + "theta": [90, 180, 360], + "opacity": 0.7 + }, { + "type": "barpolar", + "r": [3, 3], + "theta": [0, 45], + "opacity": 0.7, + "marker": { + "line": {"width": 5} + } + }], + "layout": { + "polar": {"barmode": "overlay"}, + "width": 500, + "height": 500 + } +} diff --git a/test/image/mocks/polar_bar-stacked.json b/test/image/mocks/polar_bar-stacked.json new file mode 100644 index 00000000000..289dbc9f80b --- /dev/null +++ b/test/image/mocks/polar_bar-stacked.json @@ -0,0 +1,17 @@ +{ + "data": [{ + "type": "barpolar", + "r": [1, 2, 3, 4], + "marker": {"line": {"width": 2}} + }, { + "type": "barpolar", + "r": [1, 2, 3, 1], + "marker": {"line": {"width": 2}} + }], + "layout": { + "polar": {"barmode": "stacked"}, + "colorway": ["#8dd3c7", "#bebada"], + "width": 500, + "height": 500 + } +} diff --git a/test/image/mocks/polar_bar-width-base-offset.json b/test/image/mocks/polar_bar-width-base-offset.json new file mode 100644 index 00000000000..b4598e572ef --- /dev/null +++ b/test/image/mocks/polar_bar-width-base-offset.json @@ -0,0 +1,107 @@ +{ + "data": [ + { + "base": [0, 1, 2, 3], + "width": 100, + "r": [1, 1, 1, 1], + "type": "barpolar" + }, { + "base": [4, 3, 2, 1], + "r": [1, 1, 1, 1], + "type": "barpolar" + }, { + "base": [0, 1, 3, 2], + "width": 25, + "r": [1, 2, 1, 2], + "type": "barpolar" + }, { + "base": [1, 3, 2, 4], + "r": [1, 2, 1, 2], + "type": "barpolar" + }, + + { + "subplot": "polar2", + "base": [100, 40, 25, 10], + "r": [-50, 10, 25, 40], + "type": "barpolar" + }, { + "subplot": "polar2", + "base": [0, 60, 75, 90], + "r": [50, -10, -25, -40], + "type": "barpolar" + }, + + { + "subplot": "polar3", + "base": [0, 1, 2, 3], + "width": [10, 8, 6, 4], + "offset": [0, 0, 2, 6], + "r": [1, 1, 1, 1], + "dtheta": 22.5, + "type": "barpolar" + }, { + "subplot": "polar3", + "base": [4, 3, 2, 1], + "width": [4, 6, 8, 10], + "offset": [2, 8, 2, 4], + "r": [-1, -1, -1, -1], + "theta0": 90, + "dtheta": 22.5, + "type": "barpolar" + }, { + "subplot": "polar3", + "base": [0, 1, 3, 2], + "width": 10, + "offset": [30], + "r": [1, 2, -1, 2], + "theta0": 180, + "dtheta": 22.5, + "type": "barpolar" + }, { + "subplot": "polar3", + "base": [1, 3, 2, 4], + "r": [-1, -2, 1, -2], + "offset": -15, + "theta0": 270, + "dtheta": 22.5, + "type": "barpolar" + }, + + { + "subplot": "polar4", + "width": [1, 0.8, 0.6, 0.4], + "r": [1, 2, 3, 4], + "thetaunit": "radians", + "type": "barpolar" + }, { + "subplot": "polar4", + "width": [0.4, 0.6, 0.8, 1], + "r": [3, 2, 1, 0], + "thetaunit": "radians", + "type": "barpolar" + } + ], + "layout": { + "height": 600, + "width": 600, + "showlegend": false, + "grid": {"rows": 2, "columns": 2, "xgap": 0.2, "ygap": 0.2}, + "polar": { + "barmode": "overlay", + "domain": {"row": 0, "column": 0} + }, + "polar2": { + "barmode": "overlay", + "domain": {"row": 0, "column": 1} + }, + "polar3": { + "barmode": "overlay", + "domain": {"row": 1, "column": 0} + }, + "polar4": { + "barmode": "stack", + "domain": {"row": 1, "column": 1} + } + } +} diff --git a/test/image/mocks/polar_dates.json b/test/image/mocks/polar_dates.json index c7336e08dc4..7af0936975c 100644 --- a/test/image/mocks/polar_dates.json +++ b/test/image/mocks/polar_dates.json @@ -47,6 +47,12 @@ "line": { "shape": "spline" } + }, + { + "type": "scatterpolar", + "subplot": "polar3", + "r": ["2018-08-1", "2018-09-5", "2018-10-8"], + "marker": {"size": 20} } ], "layout": { @@ -71,9 +77,19 @@ "direction": "clockwise" } }, + "xaxis": { + "domain": [0, 0.46] + }, "yaxis": { "domain": [0, 0.46] }, + "polar3": { + "domain": { + "x": [0.54, 1], + "y": [0, 0.44] + }, + "radialaxis": {"range": ["2018-07-01", "2018-10-05"]} + }, "hovermode": "closest", "showlegend": false, "width": 650, diff --git a/test/image/mocks/polar_funky-bars.json b/test/image/mocks/polar_funky-bars.json new file mode 100644 index 00000000000..e63f8abca6b --- /dev/null +++ b/test/image/mocks/polar_funky-bars.json @@ -0,0 +1,63 @@ +{ + "data": [ + { + "type": "barpolar", + "name": "on reversed-ranged radial axis", + "r": [10, 12, 15] + }, + { + "type": "barpolar", + "name": "on log radial axis", + "subplot": "polar2", + "r": [100, 50, 200] + }, + { + "type": "barpolar", + "name": "on category radial axis", + "subplot": "polar3", + "r": ["a", "b", "c", "d", "b", "f", "a", "a"] + }, + { + "type": "barpolar", + "name": "on date radial axis", + "subplot": "polar4", + "r": ["2018-08-1", "2018-09-5", "2018-10-8", "2018-09-20"] + } + ], + "layout": { + "width": 600, + "height": 600, + "margin": {"l": 40, "r": 40, "b": 60, "t": 20, "pad": 0}, + "grid": { + "rows": 2, + "columns": 2, + "ygap": 0.2 + }, + "polar": { + "domain": {"row": 0, "column": 0}, + "radialaxis": {"range": [20, 0]} + }, + "polar2": { + "domain": {"row": 0, "column": 1}, + "radialaxis": {"type": "log"}, + "angularaxis": {"direction": "clockwise"} + }, + "polar3": { + "domain": {"row": 1, "column": 0}, + "radialaxis": { + "categoryorder": "category descending" + } + }, + "polar4": { + "domain": {"row": 1, "column": 1}, + "angularaxis": {"direction": "clockwise"}, + "radialaxis": {"angle": 90, "side": "counterclockwise"} + }, + "legend": { + "x": 0.5, + "y": -0.05, + "xanchor": "center", + "yanchor": "top" + } + } +} diff --git a/test/image/mocks/polar_polygon-bars.json b/test/image/mocks/polar_polygon-bars.json new file mode 100644 index 00000000000..2f8cd0fe0f7 --- /dev/null +++ b/test/image/mocks/polar_polygon-bars.json @@ -0,0 +1,89 @@ +{ + "data": [ + { + "type": "barpolar", + "r": [1, 2, 1, 2, 3, 2, 1, 3], + "theta": ["a", "b", "c", "d", "f", "g", "h", "i"] + }, + { + "type": "barpolar", + "r": [3, 1, 2, 3, 2, 1, 2, 1], + "theta": ["a", "b", "c", "d", "f", "g", "h", "i"] + }, + { + "type": "barpolar", + "subplot": "polar2", + "r": [1, 2, 1, 2, 3, 2, 1], + "theta": ["a", "b", "c", "d", "f", "g", "h"] + }, + { + "type": "barpolar", + "subplot": "polar2", + "r": [1, 2, 3, 2, 1, 2, 1], + "theta": ["a", "b", "c", "d", "f", "g", "h"] + }, + { + "type": "barpolar", + "subplot": "polar3", + "r": [1, 2, 1, 2, 3, 2, 1, 3], + "theta": ["a", "b", "c", "d", "f", "g", "h", "i"], + "width": 0.9, + "opacity": 0.6 + }, + { + "type": "barpolar", + "subplot": "polar3", + "r": [3, 1, 2, 3, 2, 1, 2, 1], + "theta": ["a", "b", "c", "d", "f", "g", "h", "i"], + "width": 0.5, + "opacity": 0.6 + }, + { + "type": "scatterpolar", + "subplot": "polar4", + "r": [1, 2, 1, 2, 3], + "theta": ["a", "b", "c", "d", "e"], + "fill": "toself" + }, + { + "type": "barpolar", + "subplot": "polar4", + "r": [1, 2, 1, 2, 3], + "theta": ["a", "b", "c", "d", "e"], + "opacity": 0.7 + } + ], + "layout": { + "showlegend": false, + "width": 600, + "height": 600, + "grid": { + "rows": 2, + "columns": 2, + "ygap": 0.2 + }, + "polar": { + "gridshape": "linear", + "bargap": 0.1, + "domain": {"row": 0, "column": 0} + }, + "polar2": { + "gridshape": "linear", + "bargap": 0, + "domain": {"row": 0, "column": 1}, + "angularaxis": {"direction": "clockwise"} + }, + "polar3": { + "gridshape": "linear", + "domain": {"row": 1, "column": 0}, + "barmode": "overlay", + "sector": [0, 180] + }, + "polar4": { + "gridshape": "linear", + "domain": {"row": 1, "column": 1}, + "angularaxis": {"direction": "clockwise"}, + "radialaxis": {"range": [0, 2]} + } + } +} diff --git a/test/image/mocks/polar_wind-rose.json b/test/image/mocks/polar_wind-rose.json new file mode 100644 index 00000000000..fb40daa5592 --- /dev/null +++ b/test/image/mocks/polar_wind-rose.json @@ -0,0 +1,38 @@ +{ + "data": [{ + "r": [77.5, 72.5, 70.0, 45.0, 22.5, 42.5, 40.0, 62.5], + "theta": ["North", "N-E", "East", "S-E", "South", "S-W", "West", "N-W"], + "name": "11-14 m/s", + "marker": {"color": "rgb(106,81,163)"}, + "type": "barpolar" + }, { + "r": [57.5, 50.0, 45.0, 35.0, 20.0, 22.5, 37.5, 55.0], + "theta": ["North", "N-E", "East", "S-E", "South", "S-W", "West", "N-W"], + "name": "8-11 m/s", + "marker": {"color": "rgb(158,154,200)"}, + "type": "barpolar" + }, { + "r": [40.0, 30.0, 30.0, 35.0, 7.5, 7.5, 32.5, 40.0], + "theta": ["North", "N-E", "East", "S-E", "South", "S-W", "West", "N-W"], + "name": "5-8 m/s", + "marker": {"color": "rgb(203,201,226)"}, + "type": "barpolar" + }, { + "r": [20.0, 7.5, 15.0, 22.5, 2.5, 2.5, 12.5, 22.5], + "theta": ["North", "N-E", "East", "S-E", "South", "S-W", "West", "N-W"], + "name": "< 5 m/s", + "marker": {"color": "rgb(242,240,247)"}, + "type": "barpolar" + }], + "layout": { + "title": "Wind Speed Distribution in Laurel, NE", + "font": {"size": 16}, + "legend": {"font": {"size": 16}}, + "polar": { + "barmode": "overlay", + "bargap": 0, + "radialaxis": {"ticksuffix": "%", "angle": 45, "dtick": 20}, + "angularaxis": {"direction": "clockwise"} + } + } +} diff --git a/test/jasmine/assets/mock_lists.js b/test/jasmine/assets/mock_lists.js index 8f7f3efc505..c54d91a4e00 100644 --- a/test/jasmine/assets/mock_lists.js +++ b/test/jasmine/assets/mock_lists.js @@ -23,6 +23,7 @@ var svgMockList = [ ['layout-colorway', require('@mocks/layout-colorway.json')], ['polar_categories', require('@mocks/polar_categories.json')], ['polar_direction', require('@mocks/polar_direction.json')], + ['polar_wind-rose', require('@mocks/polar_wind-rose.json')], ['range_selector_style', require('@mocks/range_selector_style.json')], ['range_slider_multiple', require('@mocks/range_slider_multiple.json')], ['sankey_energy', require('@mocks/sankey_energy.json')], diff --git a/test/jasmine/tests/barpolar_test.js b/test/jasmine/tests/barpolar_test.js new file mode 100644 index 00000000000..a6aa3ef7f5f --- /dev/null +++ b/test/jasmine/tests/barpolar_test.js @@ -0,0 +1,327 @@ +var Plotly = require('@lib'); +var Lib = require('@src/lib'); + +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); +var failTest = require('../assets/fail_test'); + +describe('Test barpolar hover:', function() { + var gd; + + afterEach(destroyGraphDiv); + + function run(specs) { + gd = createGraphDiv(); + + var data = specs.traces.map(function(t) { + t.type = 'barpolar'; + return t; + }); + + var layout = Lib.extendFlat({ + showlegend: false, + width: 400, + height: 400, + margin: {t: 0, b: 0, l: 0, r: 0, pad: 0} + }, specs.layout || {}); + + return Plotly.plot(gd, data, layout).then(function() { + var subplot = gd._fullLayout.polar._subplot; + + var results = gd.calcdata.map(function(cd) { + var trace = cd[0].trace; + var pointData = { + index: false, + distance: 20, + cd: cd, + trace: trace, + subplot: subplot, + maxHoverDistance: 20 + }; + var pts = trace._module.hoverPoints(pointData, specs.xval, specs.yval, 'closest'); + return pts ? pts[0] : {distance: Infinity}; + }); + + // assert closest point (the one corresponding to the hover label) + // if two points are equidistant, pick pt of 'above' trace + results.sort(function(a, b) { + return a.distance - b.distance || b.trace.index - a.trace.index; + }); + var actual = results[0]; + var exp = specs.exp; + + for(var k in exp) { + var msg = '- key ' + k; + if(k === 'x') { + expect(actual.x0).toBe(exp.x, msg); + expect(actual.x1).toBe(exp.x, msg); + } else if(k === 'y') { + expect(actual.y0).toBe(exp.y, msg); + expect(actual.y1).toBe(exp.y, msg); + } else if(k === 'curveNumber') { + expect(actual.trace.index).toBe(exp.curveNumber, msg); + } else { + expect(actual[k]).toBe(exp[k], msg); + } + } + }); + } + + [{ + desc: 'base', + traces: [{ + r: [1, 2, 3], + theta: [0, 90, 180] + }], + xval: 1, + yval: 0, + exp: { + index: 0, + x: 263.33, + y: 200, + extraText: 'r: 1
θ: 0°', + color: '#1f77b4' + } + }, { + desc: 'works with bars with offsets', + traces: [{ + r: [1, 2, 3], + theta: [0, 90, 180], + offset: -90 + }], + xval: 1, + yval: 0, + exp: { + index: 1, + x: 296.32, + y: 117.74, + extraText: 'r: 2
θ: 90°', + color: '#1f77b4' + } + }, { + desc: 'works on clockwise angular axes', + traces: [{ + r: [1, 2, 3], + theta: [0, 90, 180], + marker: {color: 'red'} + }], + layout: { + polar: { + angularaxis: {direction: 'clockwise'} + } + }, + xval: 0, + yval: 1, + exp: { + index: 0, + x: 200, + y: 136.67, + extraText: 'r: 1
θ: 0°', + color: 'red' + } + }, { + desc: 'works with radians theta coordinates', + traces: [{ + r: [1, 2, 3], + theta: [0, 2, 4], + thetaunit: 'radians' + }], + xval: 1, + yval: 0, + exp: { + index: 0, + x: 263.33, + y: 200, + extraText: 'r: 1
θ: 0°', + color: '#1f77b4' + } + }, { + desc: 'works on radians angular axes', + traces: [{ + r: [1, 2, 3], + theta: [0, 90, 180], + marker: { + color: 'rgba(255,0,0,0)', + line: {width: 2, color: 'green'} + } + }], + layout: { + polar: { + angularaxis: {thetaunit: 'radians'} + } + }, + xval: -1, + yval: 0, + exp: { + index: 2, + x: 10, + y: 200, + extraText: 'r: 3
θ: 3.141593', + color: 'green' + } + }, { + desc: 'works on category angular axes', + traces: [{ + theta: ['a', 'b', 'c', 'd', 'e'], + r0: 1 + }], + xval: 1, + yval: 0, + exp: { + index: 0, + x: 238, + y: 200, + extraText: 'r: 1
θ: a', + color: '#1f77b4' + } + }, { + desc: 'works on *gridshape:linear* subplots', + traces: [{ + theta: ['a', 'b', 'c', 'd', 'e'], + r0: 1 + }], + layout: { + polar: {gridshape: 'linear'} + }, + xval: 1, + yval: 0, + exp: { + index: 0, + x: 238, + y: 200, + extraText: 'r: 1
θ: a', + color: '#1f77b4' + } + }, { + desc: 'works on log radial axes', + traces: [{ + r: [100, 200, 50] + }], + layout: { + polar: { + radialaxis: {type: 'log'} + } + }, + xval: 0, + yval: 0.3, + exp: { + index: 1, + x: 105, + y: 35.46, + extraText: 'r: 200
θ: 120°', + color: '#1f77b4' + } + }, { + desc: 'works on category radial axes', + traces: [{ + r: ['a', 'b', 'd', 'f'] + }], + xval: -2, + yval: 0, + exp: { + index: 2, + x: 70, + y: 200, + extraText: 'r: d
θ: 180°', + color: '#1f77b4' + } + }, { + desc: 'works on date radial axes', + traces: [{ + r: ['2018-08-1', '2018-09-5', '2018-10-8', '2018-09-20'] + }], + xval: 0, + yval: 3295437499, + exp: { + index: 1, + x: 200, + y: 97.35, + extraText: 'r: Sep 5, 2018
θ: 90°', + color: '#1f77b4' + } + }, { + desc: 'works on negative radial coordinates', + traces: [{ + base: 10, + r: [-1, -5, 10, -5] + }], + xval: 0, + yval: -6, + exp: { + index: 3, + x: 200, + y: 247.5, + extraText: 'r: −5
θ: 270°', + color: '#1f77b4', + idealAlign: 'left' + } + }, { + desc: 'works on reversed radial axis ranges', + traces: [{ + r: [10, 12, 15] + }], + layout: { + polar: { + radialaxis: {range: [20, 0]} + } + }, + xval: 8, + yval: -8, + exp: { + index: 1, + x: 160, + y: 130.72, + extraText: 'r: 12
θ: 120°', + color: '#1f77b4' + } + }, { + desc: 'on overlapping bars of same size, the narrower wins', + traces: [{ + r: [1, 2, 3], + theta: [90, 180, 360], + width: 70 + }, { + r: [1, 2, 3], + theta: [90, 180, 360], + width: 90 + }], + layout: {polar: {barmode: 'overlay'}}, + xval: 1, + yval: 0, + exp: { + curveNumber: 0, + index: 2, + x: 390, + y: 200, + extraText: 'r: 3
θ: 360°', + color: '#1f77b4' + } + }, { + desc: 'on overlapping bars of same width, the one will tip closer to cursor wins', + traces: [{ + r: [1, 2, 2], + theta: [90, 180, 360], + width: 70 + }, { + r: [1, 2, 3], + theta: [90, 180, 360], + width: 70 + }], + layout: {polar: {barmode: 'overlay'}}, + xval: 1.9, + yval: 0, + exp: { + curveNumber: 0, + index: 2, + x: 326.67, + y: 200, + extraText: 'r: 2
θ: 360°', + color: '#1f77b4' + } + }] + .forEach(function(specs) { + it('should generate correct hover labels ' + specs.desc, function(done) { + run(specs).catch(failTest).then(done); + }); + }); +}); diff --git a/test/jasmine/tests/histogram_test.js b/test/jasmine/tests/histogram_test.js index 327b4d2feb7..e39096a7c94 100644 --- a/test/jasmine/tests/histogram_test.js +++ b/test/jasmine/tests/histogram_test.js @@ -206,7 +206,7 @@ describe('Test histogram', function() { var out = calc(gd, fullTrace); delete out[0].trace; - // this is dumb - but some of the `p0` values are `-0` which doesn't match `0` + // this is dumb - but some of the `ph0` values are `-0` which doesn't match `0` // even though -0 === 0 out.forEach(function(cdi) { for(var key in cdi) { @@ -238,10 +238,10 @@ describe('Test histogram', function() { expect(out).toEqual([ // full calcdata has x and y too (and t in the first one), // but those come later from crossTraceCalc. - {i: 0, b: 0, p: d70, s: 2, pts: [0, 1], p0: d70, p1: d70}, - {i: 1, b: 0, p: d71, s: 1, pts: [2], p0: d71, p1: d71}, - {i: 2, b: 0, p: d72, s: 0, pts: [], p0: d72, p1: d72}, - {i: 3, b: 0, p: d73, s: 1, pts: [3], p0: d73, p1: d73} + {i: 0, b: 0, p: d70, s: 2, pts: [0, 1], ph0: d70, ph1: d70}, + {i: 1, b: 0, p: d71, s: 1, pts: [2], ph0: d71, ph1: d71}, + {i: 2, b: 0, p: d72, s: 0, pts: [], ph0: d72, ph1: d72}, + {i: 3, b: 0, p: d73, s: 1, pts: [3], ph0: d73, ph1: d73} ]); // All data on exact months: shift so bin center is on (31-day months) @@ -255,10 +255,10 @@ describe('Test histogram', function() { var d70mar = Date.UTC(1970, 2, 2, 12); var d70apr = Date.UTC(1970, 3, 1); expect(out).toEqual([ - {i: 0, b: 0, p: d70, s: 2, pts: [0, 1], p0: d70, p1: d70}, - {i: 1, b: 0, p: d70feb, s: 1, pts: [2], p0: d70feb, p1: d70feb}, - {i: 2, b: 0, p: d70mar, s: 0, pts: [], p0: d70mar, p1: d70mar}, - {i: 3, b: 0, p: d70apr, s: 1, pts: [3], p0: d70apr, p1: d70apr} + {i: 0, b: 0, p: d70, s: 2, pts: [0, 1], ph0: d70, ph1: d70}, + {i: 1, b: 0, p: d70feb, s: 1, pts: [2], ph0: d70feb, ph1: d70feb}, + {i: 2, b: 0, p: d70mar, s: 0, pts: [], ph0: d70mar, ph1: d70mar}, + {i: 3, b: 0, p: d70apr, s: 1, pts: [3], ph0: d70apr, ph1: d70apr} ]); // data on exact days: shift so each bin goes from noon to noon @@ -272,11 +272,11 @@ describe('Test histogram', function() { expect(out).toEqual([ // dec 31 12:00 -> jan 31 12:00, middle is jan 16 - {i: 0, b: 0, p: Date.UTC(1970, 0, 16), s: 2, pts: [0, 1], p0: Date.UTC(1970, 0, 1), p1: Date.UTC(1970, 0, 31)}, + {i: 0, b: 0, p: Date.UTC(1970, 0, 16), s: 2, pts: [0, 1], ph0: Date.UTC(1970, 0, 1), ph1: Date.UTC(1970, 0, 31)}, // jan 31 12:00 -> feb 28 12:00, middle is feb 14 12:00 - {i: 1, b: 0, p: Date.UTC(1970, 1, 14, 12), s: 1, pts: [2], p0: Date.UTC(1970, 1, 1), p1: Date.UTC(1970, 1, 28)}, - {i: 2, b: 0, p: Date.UTC(1970, 2, 16), s: 0, pts: [], p0: Date.UTC(1970, 2, 1), p1: Date.UTC(1970, 2, 31)}, - {i: 3, b: 0, p: Date.UTC(1970, 3, 15, 12), s: 1, pts: [3], p0: Date.UTC(1970, 3, 1), p1: Date.UTC(1970, 3, 30)} + {i: 1, b: 0, p: Date.UTC(1970, 1, 14, 12), s: 1, pts: [2], ph0: Date.UTC(1970, 1, 1), ph1: Date.UTC(1970, 1, 28)}, + {i: 2, b: 0, p: Date.UTC(1970, 2, 16), s: 0, pts: [], ph0: Date.UTC(1970, 2, 1), ph1: Date.UTC(1970, 2, 31)}, + {i: 3, b: 0, p: Date.UTC(1970, 3, 15, 12), s: 1, pts: [3], ph0: Date.UTC(1970, 3, 1), ph1: Date.UTC(1970, 3, 30)} ]); }); @@ -292,10 +292,10 @@ describe('Test histogram', function() { x3 = x2 + oneDay; expect(out).toEqual([ - {i: 0, b: 0, p: x0, s: 2, pts: [0, 1], p0: x0, p1: x0}, - {i: 1, b: 0, p: x1, s: 1, pts: [2], p0: x1, p1: x1}, - {i: 2, b: 0, p: x2, s: 0, pts: [], p0: x2, p1: x2}, - {i: 3, b: 0, p: x3, s: 1, pts: [3], p0: x3, p1: x3} + {i: 0, b: 0, p: x0, s: 2, pts: [0, 1], ph0: x0, ph1: x0}, + {i: 1, b: 0, p: x1, s: 1, pts: [2], ph0: x1, ph1: x1}, + {i: 2, b: 0, p: x2, s: 0, pts: [], ph0: x2, ph1: x2}, + {i: 3, b: 0, p: x3, s: 1, pts: [3], ph0: x3, ph1: x3} ]); }); @@ -319,7 +319,7 @@ describe('Test histogram', function() { }); expect(out).toEqual([ - {i: 0, b: 0, p: 3, s: 3, width1: 2, pts: [0, 1, 2], p0: 2, p1: 3.9} + {i: 0, b: 0, p: 3, s: 3, width1: 2, pts: [0, 1, 2], ph0: 2, ph1: 3.9} ]); }); @@ -332,7 +332,7 @@ describe('Test histogram', function() { }); expect(out).toEqual([ - {i: 0, b: 0, p: 1.1, s: 3, width1: 0.5, pts: [0, 1, 2], p0: 1.1, p1: 1.1} + {i: 0, b: 0, p: 1.1, s: 3, width1: 0.5, pts: [0, 1, 2], ph0: 1.1, ph1: 1.1} ]); }); @@ -345,7 +345,7 @@ describe('Test histogram', function() { }); expect(out).toEqual([ - {i: 0, b: 0, p: 17, s: 2, width1: 2, pts: [2, 4], p0: 17, p1: 17} + {i: 0, b: 0, p: 17, s: 2, width1: 2, pts: [2, 4], ph0: 17, ph1: 17} ]); }); @@ -358,7 +358,7 @@ describe('Test histogram', function() { }); expect(out).toEqual([ - {i: 0, b: 0, p: 13, s: 2, width1: 8, pts: [1, 3], p0: 13, p1: 13} + {i: 0, b: 0, p: 13, s: 2, width1: 8, pts: [1, 3], ph0: 13, ph1: 13} ]); }); @@ -372,7 +372,7 @@ describe('Test histogram', function() { var p = 1296691200000; expect(out).toEqual([ - {i: 0, b: 0, p: p, s: 2, width1: 2 * 24 * 3600 * 1000, pts: [1, 3], p0: p, p1: p} + {i: 0, b: 0, p: p, s: 2, width1: 2 * 24 * 3600 * 1000, pts: [1, 3], ph0: p, ph1: p} ]); }); @@ -385,7 +385,7 @@ describe('Test histogram', function() { }); expect(out).toEqual([ - {i: 0, b: 0, p: 97, s: 2, width1: 1, pts: [1, 3], p0: 97, p1: 97} + {i: 0, b: 0, p: 97, s: 2, width1: 1, pts: [1, 3], ph0: 97, ph1: 97} ]); }); @@ -393,7 +393,7 @@ describe('Test histogram', function() { var out = _calc({x: [1, 4]}, [], {barmode: 'overlay'}); expect(out).toEqual([ - {i: 0, b: 0, p: 2, s: 2, width1: 5, pts: [0, 1], p0: 0, p1: 4} + {i: 0, b: 0, p: 2, s: 2, width1: 5, pts: [0, 1], ph0: 0, ph1: 4} ]); // real single-valued trace inherits bar width from the simply single-bin trace @@ -404,7 +404,7 @@ describe('Test histogram', function() { }); expect(out).toEqual([ - {i: 0, b: 0, p: 5, s: 1, width1: 5, pts: [0], p0: 5, p1: 5} + {i: 0, b: 0, p: 5, s: 1, width1: 5, pts: [0], ph0: 5, ph1: 5} ]); }); @@ -496,14 +496,14 @@ describe('Test histogram', function() { it('makes the right base histogram', function() { var baseOut = _calc(base); expect(baseOut).toEqual([ - {i: 0, b: 0, p: 2, s: 1, pts: [0], p0: 0, p1: 0}, - {i: 1, b: 0, p: 7, s: 2, pts: [1, 4], p0: 5, p1: 5}, - {i: 2, b: 0, p: 12, s: 3, pts: [2, 5, 7], p0: 10, p1: 10}, - {i: 3, b: 0, p: 17, s: 4, pts: [3, 6, 8, 9], p0: 15, p1: 15}, + {i: 0, b: 0, p: 2, s: 1, pts: [0], ph0: 0, ph1: 0}, + {i: 1, b: 0, p: 7, s: 2, pts: [1, 4], ph0: 5, ph1: 5}, + {i: 2, b: 0, p: 12, s: 3, pts: [2, 5, 7], ph0: 10, ph1: 10}, + {i: 3, b: 0, p: 17, s: 4, pts: [3, 6, 8, 9], ph0: 15, ph1: 15}, ]); }); - // p0, p1, and pts have been omitted from CDFs for now + // ph0, ph1, and pts have been omitted from CDFs for now var CDFs = [ {p: [2, 7, 12, 17], s: [1, 3, 6, 10]}, { diff --git a/test/jasmine/tests/select_test.js b/test/jasmine/tests/select_test.js index 494ae419de4..821c7380e96 100644 --- a/test/jasmine/tests/select_test.js +++ b/test/jasmine/tests/select_test.js @@ -1282,6 +1282,62 @@ describe('Test select box and lasso per trace:', function() { .then(done); }); + it('@flaky should work on barpolar traces', function(done) { + var assertPoints = makeAssertPoints(['r', 'theta']); + var assertSelectedPoints = makeAssertSelectedPoints(); + + var fig = Lib.extendDeep({}, require('@mocks/polar_wind-rose.json')); + fig.layout.showlegend = false; + fig.layout.width = 500; + fig.layout.height = 500; + fig.layout.dragmode = 'select'; + addInvisible(fig); + + Plotly.plot(gd, fig).then(function() { + return _run( + [[150, 150], [250, 250]], + function() { + assertPoints([ + [62.5, 'N-W'], [55, 'N-W'], [40, 'North'], + [40, 'N-W'], [20, 'North'], [22.5, 'N-W'] + ]); + assertSelectedPoints({ + 0: [7], + 1: [7], + 2: [0, 7], + 3: [0, 7] + }); + }, + [200, 200], + BOXEVENTS, 'barpolar select' + ); + }) + .then(function() { + return Plotly.relayout(gd, 'dragmode', 'lasso'); + }) + .then(function() { + return _run( + [[150, 150], [350, 150], [350, 250], [150, 250], [150, 150]], + function() { + assertPoints([ + [62.5, 'N-W'], [50, 'N-E'], [55, 'N-W'], [40, 'North'], + [30, 'N-E'], [40, 'N-W'], [20, 'North'], [7.5, 'N-E'], [22.5, 'N-W'] + ]); + assertSelectedPoints({ + 0: [7], + 1: [1, 7], + 2: [0, 1, 7], + 3: [0, 1, 7] + }); + }, + [200, 200], + LASSOEVENTS, 'barpolar lasso' + ); + }) + .catch(failTest) + .then(done); + }); + it('@flaky should work on choropleth traces', function(done) { var assertPoints = makeAssertPoints(['location', 'z']); var assertSelectedPoints = makeAssertSelectedPoints();