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