Skip to content

Commit 46fe066

Browse files
committed
first cut polar subplot defaults and draw routines
1 parent ca8081b commit 46fe066

File tree

7 files changed

+1069
-10
lines changed

7 files changed

+1069
-10
lines changed

src/plots/cartesian/axes.js

+116-9
Original file line numberDiff line numberDiff line change
@@ -765,6 +765,10 @@ axes.calcTicks = function calcTicks(ax) {
765765
minPx = ax._id.charAt(0) === 'y' ? 40 : 80;
766766
nt = Lib.constrain(ax._length / minPx, 4, 9) + 1;
767767
}
768+
769+
// radial axes span half their domain,
770+
// multiply nticks value by two to get correct number of auto ticks.
771+
if(ax._name === 'radialaxis') nt *= 2;
768772
}
769773

770774
// add a couple of extra digits for filling in ticks when we
@@ -819,6 +823,13 @@ axes.calcTicks = function calcTicks(ax) {
819823
vals.push(x);
820824
}
821825

826+
// If same angle over a full circle, the last tick vals is a duplicate.
827+
//
828+
// TODO must do something similar for angular date axes.
829+
if(ax._id === 'angular' && Math.abs(rng[1] - rng[0]) === 360) {
830+
vals.pop();
831+
}
832+
822833
// save the last tick as well as first, so we can
823834
// show the exponent only on the last one
824835
ax._tmax = vals[vals.length - 1];
@@ -886,7 +897,11 @@ var roundBase10 = [2, 5, 10],
886897
// approx. tick positions for log axes, showing all (1) and just 1, 2, 5 (2)
887898
// these don't have to be exact, just close enough to round to the right value
888899
roundLog1 = [-0.046, 0, 0.301, 0.477, 0.602, 0.699, 0.778, 0.845, 0.903, 0.954, 1],
889-
roundLog2 = [-0.301, 0, 0.301, 0.699, 1];
900+
roundLog2 = [-0.301, 0, 0.301, 0.699, 1],
901+
// TODO
902+
// maybe [1, 2, 5, 10, 15, 30, 45, 90, 180] would give better results
903+
// on thin polar sectors?
904+
roundAngles = [15, 30, 45, 90, 180];
890905

891906
function roundDTick(roughDTick, base, roundingSet) {
892907
return base * Lib.roundUp(roughDTick / base, roundingSet);
@@ -911,6 +926,10 @@ function roundDTick(roughDTick, base, roundingSet) {
911926
axes.autoTicks = function(ax, roughDTick) {
912927
var base;
913928

929+
function getBase(v) {
930+
return Math.pow(v, Math.floor(Math.log(roughDTick) / Math.LN10));
931+
}
932+
914933
if(ax.type === 'date') {
915934
ax.tick0 = Lib.dateTick0(ax.calendar);
916935
// the criteria below are all based on the rough spacing we calculate
@@ -919,7 +938,7 @@ axes.autoTicks = function(ax, roughDTick) {
919938

920939
if(roughX2 > ONEAVGYEAR) {
921940
roughDTick /= ONEAVGYEAR;
922-
base = Math.pow(10, Math.floor(Math.log(roughDTick) / Math.LN10));
941+
base = getBase(10);
923942
ax.dtick = 'M' + (12 * roundDTick(roughDTick, base, roundBase10));
924943
}
925944
else if(roughX2 > ONEAVGMONTH) {
@@ -944,7 +963,7 @@ axes.autoTicks = function(ax, roughDTick) {
944963
}
945964
else {
946965
// milliseconds
947-
base = Math.pow(10, Math.floor(Math.log(roughDTick) / Math.LN10));
966+
base = getBase(10);
948967
ax.dtick = roundDTick(roughDTick, base, roundBase10);
949968
}
950969
}
@@ -963,7 +982,7 @@ axes.autoTicks = function(ax, roughDTick) {
963982
// ticks on a linear scale, labeled fully
964983
roughDTick = Math.abs(Math.pow(10, rng[1]) -
965984
Math.pow(10, rng[0])) / nt;
966-
base = Math.pow(10, Math.floor(Math.log(roughDTick) / Math.LN10));
985+
base = getBase(10);
967986
ax.dtick = 'L' + roundDTick(roughDTick, base, roundBase10);
968987
}
969988
else {
@@ -977,10 +996,15 @@ axes.autoTicks = function(ax, roughDTick) {
977996
ax.tick0 = 0;
978997
ax.dtick = Math.ceil(Math.max(roughDTick, 1));
979998
}
999+
else if(ax._id === 'angular') {
1000+
ax.tick0 = 0;
1001+
base = getBase(1);
1002+
ax.dtick = roundDTick(roughDTick, base, roundAngles);
1003+
}
9801004
else {
9811005
// auto ticks always start at 0
9821006
ax.tick0 = 0;
983-
base = Math.pow(10, Math.floor(Math.log(roughDTick) / Math.LN10));
1007+
base = getBase(10);
9841008
ax.dtick = roundDTick(roughDTick, base, roundBase10);
9851009
}
9861010

@@ -1208,6 +1232,7 @@ axes.tickText = function(ax, x, hover) {
12081232
if(ax.type === 'date') formatDate(ax, out, hover, extraPrecision);
12091233
else if(ax.type === 'log') formatLog(ax, out, hover, extraPrecision, hideexp);
12101234
else if(ax.type === 'category') formatCategory(ax, out);
1235+
else if(ax._id === 'angular') formatAngle(ax, out, hover, extraPrecision, hideexp);
12111236
else formatLinear(ax, out, hover, extraPrecision, hideexp);
12121237

12131238
// add prefix and suffix
@@ -1402,6 +1427,66 @@ function formatLinear(ax, out, hover, extraPrecision, hideexp) {
14021427
out.text = numFormat(out.x, ax, hideexp, extraPrecision);
14031428
}
14041429

1430+
function formatAngle(ax, out, hover, extraPrecision, hideexp) {
1431+
if(ax.thetaunit === 'radians' && !hover) {
1432+
var isNeg = out.x < 0;
1433+
var num = out.x / 180;
1434+
1435+
if(num === 0) {
1436+
out.text = '0';
1437+
} else {
1438+
var frac = num2frac(num);
1439+
1440+
if(frac[1] === 1) {
1441+
if(frac[0] === 1) out.text = 'π';
1442+
else out.text = frac[0] + 'π';
1443+
} else {
1444+
out.text = [
1445+
'<sup>', frac[0], '</sup>',
1446+
'⁄',
1447+
'<sub>', frac[1], '</sub>',
1448+
'π'
1449+
].join('');
1450+
}
1451+
}
1452+
1453+
if(isNeg) out.text = MINUS_SIGN + out.text;
1454+
} else {
1455+
out.text = numFormat(out.x, ax, hideexp, extraPrecision);
1456+
}
1457+
}
1458+
1459+
// inspired by
1460+
// https://github.com/yisibl/num2fraction/blob/master/index.js
1461+
function num2frac(num) {
1462+
function almostEq(a, b) {
1463+
return Math.abs(a - b) <= 1e-6;
1464+
}
1465+
1466+
function findGCD(a, b) {
1467+
return almostEq(b, 0) ? a : findGCD(b, a % b);
1468+
}
1469+
1470+
function findPrecision(n) {
1471+
var e = 1;
1472+
while(!almostEq(Math.round(n * e) / e, n)) {
1473+
e *= 10;
1474+
}
1475+
return e;
1476+
}
1477+
1478+
var precision = findPrecision(num);
1479+
var number = num * precision;
1480+
var gcd = Math.abs(findGCD(number, precision));
1481+
1482+
return [
1483+
// numerator
1484+
Math.round(number / gcd),
1485+
// denominator
1486+
Math.round(precision / gcd)
1487+
];
1488+
}
1489+
14051490
// format a number (tick value) according to the axis settings
14061491
// new, more reliable procedure than d3.round or similar:
14071492
// add half the rounding increment, then stringify and truncate
@@ -1846,7 +1931,7 @@ axes.doTicks = function(gd, axid, skipTitle) {
18461931
// positioning arguments for x vs y axes
18471932
if(axLetter === 'x') {
18481933
sides = ['bottom', 'top'];
1849-
transfn = function(d) {
1934+
transfn = ax._transfn || function(d) {
18501935
return 'translate(' + ax.l2p(d.x) + ',0)';
18511936
};
18521937
tickpathfn = function(shift, len) {
@@ -1859,7 +1944,7 @@ axes.doTicks = function(gd, axid, skipTitle) {
18591944
}
18601945
else if(axLetter === 'y') {
18611946
sides = ['left', 'right'];
1862-
transfn = function(d) {
1947+
transfn = ax._transfn || function(d) {
18631948
return 'translate(0,' + ax.l2p(d.x) + ')';
18641949
};
18651950
tickpathfn = function(shift, len) {
@@ -1870,6 +1955,13 @@ axes.doTicks = function(gd, axid, skipTitle) {
18701955
else return 'M' + shift + ',0h' + len;
18711956
};
18721957
}
1958+
else if(axid === 'angular') {
1959+
sides = ['left', 'right'];
1960+
transfn = ax._transfn;
1961+
tickpathfn = function(shift, len) {
1962+
return 'M' + shift + ',0h' + len;
1963+
};
1964+
}
18731965
else {
18741966
Lib.warn('Unrecognized doTicks axis:', axid);
18751967
return;
@@ -1893,6 +1985,11 @@ axes.doTicks = function(gd, axid, skipTitle) {
18931985
}
18941986
var valsClipped = vals.filter(clipEnds);
18951987

1988+
// don't clip angular values
1989+
if(ax._id === 'angular') {
1990+
valsClipped = vals;
1991+
}
1992+
18961993
function drawTicks(container, tickpath) {
18971994
var ticks = container.selectAll('path.' + tcls)
18981995
.data(ax.ticks === 'inside' ? valsClipped : vals, datafn);
@@ -1941,7 +2038,7 @@ axes.doTicks = function(gd, axid, skipTitle) {
19412038
return (angle * flipit < 0) ? 'end' : 'start';
19422039
};
19432040
}
1944-
else {
2041+
else if(axLetter === 'y') {
19452042
flipit = (axside === 'right') ? 1 : -1;
19462043
labely = function(d) {
19472044
return d.dy + d.fontSize * MID_SHIFT - labelShift * flipit;
@@ -1957,6 +2054,16 @@ axes.doTicks = function(gd, axid, skipTitle) {
19572054
return axside === 'right' ? 'start' : 'end';
19582055
};
19592056
}
2057+
else if(axid === 'angular') {
2058+
ax._labelShift = labelShift;
2059+
ax._labelStandoff = labelStandoff;
2060+
ax._pad = pad;
2061+
2062+
labelx = ax._labelx;
2063+
labely = ax._labely;
2064+
labelanchor = ax._labelanchor;
2065+
}
2066+
19602067
var maxFontSize = 0,
19612068
autoangle = 0,
19622069
labelsReady = [];
@@ -1996,7 +2103,7 @@ axes.doTicks = function(gd, axid, skipTitle) {
19962103

19972104
function positionLabels(s, angle) {
19982105
s.each(function(d) {
1999-
var anchor = labelanchor(angle);
2106+
var anchor = labelanchor(angle, d);
20002107
var thisLabel = d3.select(this),
20012108
mathjaxGroup = thisLabel.select('.text-math-group'),
20022109
transform = transfn(d) +

src/plots/polar/constants.js

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* Copyright 2012-2017, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
module.exports = {
12+
attr: 'subplot',
13+
name: 'polar',
14+
15+
axisNames: ['angularaxis', 'radialaxis'],
16+
axisName2dataArray: {angularaxis: 'theta', radialaxis: 'r'},
17+
18+
// TODO should radial axis be above frontplot by default?
19+
layerNames: [
20+
'draglayer',
21+
'plotbg',
22+
'backplot',
23+
'grids',
24+
'axes',
25+
'lines',
26+
'frontplot'
27+
],
28+
};

src/plots/polar/index.js

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* Copyright 2012-2017, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
var Plots = require('../../plots/plots');
12+
var counterRegex = require('../../lib').counterRegex;
13+
14+
var createPolar = require('./polar');
15+
var constants = require('./constants');
16+
17+
var attr = constants.attr;
18+
var name = constants.name;
19+
var counter = counterRegex(name);
20+
21+
var attributes = {};
22+
attributes[attr] = {
23+
valType: 'subplotid',
24+
role: 'info',
25+
dflt: name,
26+
editType: 'calc',
27+
description: [
28+
'Sets a reference between this trace\'s data coordinates and',
29+
'a polar subplot.',
30+
'If *polar* (the default value), the data refer to `layout.polar`.',
31+
'If *polar2*, the data refer to `layout.polar2`, and so on.'
32+
].join(' ')
33+
};
34+
35+
function plot(gd) {
36+
var fullLayout = gd._fullLayout;
37+
var calcData = gd.calcdata;
38+
var subplotIds = Plots.getSubplotIds(fullLayout, name);
39+
40+
for(var i = 0; i < subplotIds.length; i++) {
41+
var id = subplotIds[i];
42+
var subplotCalcData = Plots.getSubplotCalcData(calcData, name, id);
43+
var subplot = fullLayout[id]._subplot;
44+
45+
if(!subplot) {
46+
subplot = createPolar(gd, id);
47+
fullLayout[id]._subplot = subplot;
48+
}
49+
50+
subplot.plot(subplotCalcData, fullLayout, gd._promises);
51+
}
52+
}
53+
54+
function clean(newFullData, newFullLayout, oldFullData, oldFullLayout) {
55+
var oldIds = Plots.getSubplotIds(oldFullLayout, name);
56+
57+
for(var i = 0; i < oldIds.length; i++) {
58+
var id = oldIds[i];
59+
var oldSubplot = oldFullLayout[id]._subplot;
60+
61+
if(!newFullLayout[id] && !!oldSubplot) {
62+
oldSubplot.framework.remove();
63+
64+
for(var k in oldSubplot.clipPaths) {
65+
oldSubplot.clipPaths[k].remove();
66+
}
67+
}
68+
}
69+
}
70+
71+
module.exports = {
72+
attr: attr,
73+
name: name,
74+
idRoot: name,
75+
idRegex: counter,
76+
attrRegex: counter,
77+
attributes: attributes,
78+
layoutAttributes: require('./layout_attributes'),
79+
supplyLayoutDefaults: require('./layout_defaults'),
80+
plot: plot,
81+
clean: clean
82+
};

0 commit comments

Comments
 (0)