-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Polar 2.0 #2200
Polar 2.0 #2200
Changes from 1 commit
58d53e8
853581e
3c8ae5b
ca8081b
46fe066
0826ee7
9770f2c
0b85af2
cbf19c1
86204a6
0027f53
4cdc301
a5a719c
da720aa
04a886b
22cf4ba
122dd0b
689414c
543b615
54b3f1c
17503cb
7b0f718
fbea869
1e769f0
f53495e
c5a3a72
61e155c
258600d
0a41da5
f3a2379
3f5e77b
65b717d
13568a3
797fec1
0a9278c
07a3af7
a263c1c
765d2b7
909d134
3e1e4dd
a96131d
b35d7ec
5751de5
86067a6
5422da9
b4af46f
20b17e5
cb78d19
25b4e7b
1e81039
e5d238d
0146c2f
c94423b
4d5a1bd
d4c7d2c
8b4c2b5
5357501
9a71ba3
f714449
cde7369
9c4c9bf
b365374
a72ce55
473643c
c2da23e
974740b
e8aec86
e8865bc
8a33b89
c7a7f59
7f7193d
816be51
18a3a0d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -765,6 +765,10 @@ axes.calcTicks = function calcTicks(ax) { | |
minPx = ax._id.charAt(0) === 'y' ? 40 : 80; | ||
nt = Lib.constrain(ax._length / minPx, 4, 9) + 1; | ||
} | ||
|
||
// radial axes span half their domain, | ||
// multiply nticks value by two to get correct number of auto ticks. | ||
if(ax._name === 'radialaxis') nt *= 2; | ||
} | ||
|
||
// add a couple of extra digits for filling in ticks when we | ||
|
@@ -819,6 +823,13 @@ axes.calcTicks = function calcTicks(ax) { | |
vals.push(x); | ||
} | ||
|
||
// If same angle over a full circle, the last tick vals is a duplicate. | ||
// | ||
// TODO must do something similar for angular date axes. | ||
if(ax._id === 'angular' && Math.abs(rng[1] - rng[0]) === 360) { | ||
vals.pop(); | ||
} | ||
|
||
// save the last tick as well as first, so we can | ||
// show the exponent only on the last one | ||
ax._tmax = vals[vals.length - 1]; | ||
|
@@ -886,7 +897,11 @@ var roundBase10 = [2, 5, 10], | |
// approx. tick positions for log axes, showing all (1) and just 1, 2, 5 (2) | ||
// these don't have to be exact, just close enough to round to the right value | ||
roundLog1 = [-0.046, 0, 0.301, 0.477, 0.602, 0.699, 0.778, 0.845, 0.903, 0.954, 1], | ||
roundLog2 = [-0.301, 0, 0.301, 0.699, 1]; | ||
roundLog2 = [-0.301, 0, 0.301, 0.699, 1], | ||
// TODO | ||
// maybe [1, 2, 5, 10, 15, 30, 45, 90, 180] would give better results | ||
// on thin polar sectors? | ||
roundAngles = [15, 30, 45, 90, 180]; | ||
|
||
function roundDTick(roughDTick, base, roundingSet) { | ||
return base * Lib.roundUp(roughDTick / base, roundingSet); | ||
|
@@ -911,6 +926,10 @@ function roundDTick(roughDTick, base, roundingSet) { | |
axes.autoTicks = function(ax, roughDTick) { | ||
var base; | ||
|
||
function getBase(v) { | ||
return Math.pow(v, Math.floor(Math.log(roughDTick) / Math.LN10)); | ||
} | ||
|
||
if(ax.type === 'date') { | ||
ax.tick0 = Lib.dateTick0(ax.calendar); | ||
// the criteria below are all based on the rough spacing we calculate | ||
|
@@ -919,7 +938,7 @@ axes.autoTicks = function(ax, roughDTick) { | |
|
||
if(roughX2 > ONEAVGYEAR) { | ||
roughDTick /= ONEAVGYEAR; | ||
base = Math.pow(10, Math.floor(Math.log(roughDTick) / Math.LN10)); | ||
base = getBase(10); | ||
ax.dtick = 'M' + (12 * roundDTick(roughDTick, base, roundBase10)); | ||
} | ||
else if(roughX2 > ONEAVGMONTH) { | ||
|
@@ -944,7 +963,7 @@ axes.autoTicks = function(ax, roughDTick) { | |
} | ||
else { | ||
// milliseconds | ||
base = Math.pow(10, Math.floor(Math.log(roughDTick) / Math.LN10)); | ||
base = getBase(10); | ||
ax.dtick = roundDTick(roughDTick, base, roundBase10); | ||
} | ||
} | ||
|
@@ -963,7 +982,7 @@ axes.autoTicks = function(ax, roughDTick) { | |
// ticks on a linear scale, labeled fully | ||
roughDTick = Math.abs(Math.pow(10, rng[1]) - | ||
Math.pow(10, rng[0])) / nt; | ||
base = Math.pow(10, Math.floor(Math.log(roughDTick) / Math.LN10)); | ||
base = getBase(10); | ||
ax.dtick = 'L' + roundDTick(roughDTick, base, roundBase10); | ||
} | ||
else { | ||
|
@@ -977,10 +996,15 @@ axes.autoTicks = function(ax, roughDTick) { | |
ax.tick0 = 0; | ||
ax.dtick = Math.ceil(Math.max(roughDTick, 1)); | ||
} | ||
else if(ax._id === 'angular') { | ||
ax.tick0 = 0; | ||
base = getBase(1); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what am I missing? Isn't There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good catch -> 7f7193d |
||
ax.dtick = roundDTick(roughDTick, base, roundAngles); | ||
} | ||
else { | ||
// auto ticks always start at 0 | ||
ax.tick0 = 0; | ||
base = Math.pow(10, Math.floor(Math.log(roughDTick) / Math.LN10)); | ||
base = getBase(10); | ||
ax.dtick = roundDTick(roughDTick, base, roundBase10); | ||
} | ||
|
||
|
@@ -1208,6 +1232,7 @@ axes.tickText = function(ax, x, hover) { | |
if(ax.type === 'date') formatDate(ax, out, hover, extraPrecision); | ||
else if(ax.type === 'log') formatLog(ax, out, hover, extraPrecision, hideexp); | ||
else if(ax.type === 'category') formatCategory(ax, out); | ||
else if(ax._id === 'angular') formatAngle(ax, out, hover, extraPrecision, hideexp); | ||
else formatLinear(ax, out, hover, extraPrecision, hideexp); | ||
|
||
// add prefix and suffix | ||
|
@@ -1402,6 +1427,66 @@ function formatLinear(ax, out, hover, extraPrecision, hideexp) { | |
out.text = numFormat(out.x, ax, hideexp, extraPrecision); | ||
} | ||
|
||
function formatAngle(ax, out, hover, extraPrecision, hideexp) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah nice, here it is 🎉 I guess even though it's currently only used for polar, there is something nice about having this in But yeah, previous comments hold: we need a cutoff to switch to decimal, and then perhaps hover can use π too! |
||
if(ax.thetaunit === 'radians' && !hover) { | ||
var isNeg = out.x < 0; | ||
var num = out.x / 180; | ||
|
||
if(num === 0) { | ||
out.text = '0'; | ||
} else { | ||
var frac = num2frac(num); | ||
|
||
if(frac[1] === 1) { | ||
if(frac[0] === 1) out.text = 'π'; | ||
else out.text = frac[0] + 'π'; | ||
} else { | ||
out.text = [ | ||
'<sup>', frac[0], '</sup>', | ||
'⁄', | ||
'<sub>', frac[1], '</sub>', | ||
'π' | ||
].join(''); | ||
} | ||
} | ||
|
||
if(isNeg) out.text = MINUS_SIGN + out.text; | ||
} else { | ||
out.text = numFormat(out.x, ax, hideexp, extraPrecision); | ||
} | ||
} | ||
|
||
// inspired by | ||
// https://github.com/yisibl/num2fraction/blob/master/index.js | ||
function num2frac(num) { | ||
function almostEq(a, b) { | ||
return Math.abs(a - b) <= 1e-6; | ||
} | ||
|
||
function findGCD(a, b) { | ||
return almostEq(b, 0) ? a : findGCD(b, a % b); | ||
} | ||
|
||
function findPrecision(n) { | ||
var e = 1; | ||
while(!almostEq(Math.round(n * e) / e, n)) { | ||
e *= 10; | ||
} | ||
return e; | ||
} | ||
|
||
var precision = findPrecision(num); | ||
var number = num * precision; | ||
var gcd = Math.abs(findGCD(number, precision)); | ||
|
||
return [ | ||
// numerator | ||
Math.round(number / gcd), | ||
// denominator | ||
Math.round(precision / gcd) | ||
]; | ||
} | ||
|
||
// format a number (tick value) according to the axis settings | ||
// new, more reliable procedure than d3.round or similar: | ||
// add half the rounding increment, then stringify and truncate | ||
|
@@ -1846,7 +1931,7 @@ axes.doTicks = function(gd, axid, skipTitle) { | |
// positioning arguments for x vs y axes | ||
if(axLetter === 'x') { | ||
sides = ['bottom', 'top']; | ||
transfn = function(d) { | ||
transfn = ax._transfn || function(d) { | ||
return 'translate(' + ax.l2p(d.x) + ',0)'; | ||
}; | ||
tickpathfn = function(shift, len) { | ||
|
@@ -1859,7 +1944,7 @@ axes.doTicks = function(gd, axid, skipTitle) { | |
} | ||
else if(axLetter === 'y') { | ||
sides = ['left', 'right']; | ||
transfn = function(d) { | ||
transfn = ax._transfn || function(d) { | ||
return 'translate(0,' + ax.l2p(d.x) + ')'; | ||
}; | ||
tickpathfn = function(shift, len) { | ||
|
@@ -1870,6 +1955,13 @@ axes.doTicks = function(gd, axid, skipTitle) { | |
else return 'M' + shift + ',0h' + len; | ||
}; | ||
} | ||
else if(axid === 'angular') { | ||
sides = ['left', 'right']; | ||
transfn = ax._transfn; | ||
tickpathfn = function(shift, len) { | ||
return 'M' + shift + ',0h' + len; | ||
}; | ||
} | ||
else { | ||
Lib.warn('Unrecognized doTicks axis:', axid); | ||
return; | ||
|
@@ -1893,6 +1985,11 @@ axes.doTicks = function(gd, axid, skipTitle) { | |
} | ||
var valsClipped = vals.filter(clipEnds); | ||
|
||
// don't clip angular values | ||
if(ax._id === 'angular') { | ||
valsClipped = vals; | ||
} | ||
|
||
function drawTicks(container, tickpath) { | ||
var ticks = container.selectAll('path.' + tcls) | ||
.data(ax.ticks === 'inside' ? valsClipped : vals, datafn); | ||
|
@@ -1941,7 +2038,7 @@ axes.doTicks = function(gd, axid, skipTitle) { | |
return (angle * flipit < 0) ? 'end' : 'start'; | ||
}; | ||
} | ||
else { | ||
else if(axLetter === 'y') { | ||
flipit = (axside === 'right') ? 1 : -1; | ||
labely = function(d) { | ||
return d.dy + d.fontSize * MID_SHIFT - labelShift * flipit; | ||
|
@@ -1957,6 +2054,16 @@ axes.doTicks = function(gd, axid, skipTitle) { | |
return axside === 'right' ? 'start' : 'end'; | ||
}; | ||
} | ||
else if(axid === 'angular') { | ||
ax._labelShift = labelShift; | ||
ax._labelStandoff = labelStandoff; | ||
ax._pad = pad; | ||
|
||
labelx = ax._labelx; | ||
labely = ax._labely; | ||
labelanchor = ax._labelanchor; | ||
} | ||
|
||
var maxFontSize = 0, | ||
autoangle = 0, | ||
labelsReady = []; | ||
|
@@ -1996,7 +2103,7 @@ axes.doTicks = function(gd, axid, skipTitle) { | |
|
||
function positionLabels(s, angle) { | ||
s.each(function(d) { | ||
var anchor = labelanchor(angle); | ||
var anchor = labelanchor(angle, d); | ||
var thisLabel = d3.select(this), | ||
mathjaxGroup = thisLabel.select('.text-math-group'), | ||
transform = transfn(d) + | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
/** | ||
* Copyright 2012-2017, 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 = { | ||
attr: 'subplot', | ||
name: 'polar', | ||
|
||
axisNames: ['angularaxis', 'radialaxis'], | ||
axisName2dataArray: {angularaxis: 'theta', radialaxis: 'r'}, | ||
|
||
// TODO should radial axis be above frontplot by default? | ||
layerNames: [ | ||
'draglayer', | ||
'plotbg', | ||
'backplot', | ||
'grids', | ||
'axes', | ||
'lines', | ||
'frontplot' | ||
], | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
/** | ||
* Copyright 2012-2017, 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 Plots = require('../../plots/plots'); | ||
var counterRegex = require('../../lib').counterRegex; | ||
|
||
var createPolar = require('./polar'); | ||
var constants = require('./constants'); | ||
|
||
var attr = constants.attr; | ||
var name = constants.name; | ||
var counter = counterRegex(name); | ||
|
||
var attributes = {}; | ||
attributes[attr] = { | ||
valType: 'subplotid', | ||
role: 'info', | ||
dflt: name, | ||
editType: 'calc', | ||
description: [ | ||
'Sets a reference between this trace\'s data coordinates and', | ||
'a polar subplot.', | ||
'If *polar* (the default value), the data refer to `layout.polar`.', | ||
'If *polar2*, the data refer to `layout.polar2`, and so on.' | ||
].join(' ') | ||
}; | ||
|
||
function plot(gd) { | ||
var fullLayout = gd._fullLayout; | ||
var calcData = gd.calcdata; | ||
var subplotIds = Plots.getSubplotIds(fullLayout, name); | ||
|
||
for(var i = 0; i < subplotIds.length; i++) { | ||
var id = subplotIds[i]; | ||
var subplotCalcData = Plots.getSubplotCalcData(calcData, name, id); | ||
var subplot = fullLayout[id]._subplot; | ||
|
||
if(!subplot) { | ||
subplot = createPolar(gd, id); | ||
fullLayout[id]._subplot = subplot; | ||
} | ||
|
||
subplot.plot(subplotCalcData, fullLayout, gd._promises); | ||
} | ||
} | ||
|
||
function clean(newFullData, newFullLayout, oldFullData, oldFullLayout) { | ||
var oldIds = Plots.getSubplotIds(oldFullLayout, name); | ||
|
||
for(var i = 0; i < oldIds.length; i++) { | ||
var id = oldIds[i]; | ||
var oldSubplot = oldFullLayout[id]._subplot; | ||
|
||
if(!newFullLayout[id] && !!oldSubplot) { | ||
oldSubplot.framework.remove(); | ||
|
||
for(var k in oldSubplot.clipPaths) { | ||
oldSubplot.clipPaths[k].remove(); | ||
} | ||
} | ||
} | ||
} | ||
|
||
module.exports = { | ||
attr: attr, | ||
name: name, | ||
idRoot: name, | ||
idRegex: counter, | ||
attrRegex: counter, | ||
attributes: attributes, | ||
layoutAttributes: require('./layout_attributes'), | ||
supplyLayoutDefaults: require('./layout_defaults'), | ||
plot: plot, | ||
clean: clean | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had to hack a few things deep into
axes.js
to get the angular axes looking the way I wanted. I hope @alexcjohnson won't mind.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if you use something else as the period of a linear angular axis? Also do we need to do something more flexible to handle nearly duplicate values so we don't get overlaps?