Skip to content
Permalink
Browse files

introduce polar subplot attributes

  • Loading branch information...
etpinard committed Dec 11, 2017
1 parent 853581e commit 3c8ae5beb88b32ab2158255a15eca5157fbb5b4e
Showing with 327 additions and 0 deletions.
  1. +327 −0 src/plots/polar/layout_attributes.js
@@ -0,0 +1,327 @@
/**
* 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 colorAttrs = require('../../components/color/attributes');
var axesAttrs = require('../cartesian/layout_attributes');
var extendFlat = require('../../lib').extendFlat;
var overrideAll = require('../../plot_api/edit_types').overrideAll;

var domainItem = {
valType: 'info_array',
role: 'info',
editType: 'plot',
items: [
{valType: 'number', min: 0, max: 1},
{valType: 'number', min: 0, max: 1}
],
dflt: [0, 1]
};

This comment has been minimized.

Copy link
@alexcjohnson

alexcjohnson Dec 12, 2017

Contributor

This shows up all over the place... 🌴 ?

This comment has been minimized.

Copy link
@etpinard

etpinard Dec 21, 2017

Author Member

done in fbea869


var axisLineGridAttr = overrideAll({
color: axesAttrs.color,
showline: extendFlat({}, axesAttrs.showline, {dflt: true}),
linecolor: axesAttrs.linecolor,
linewidth: axesAttrs.linewidth,
showgrid: extendFlat({}, axesAttrs.showgrid, {dflt: true}),
gridcolor: axesAttrs.gridcolor,
gridwidth: axesAttrs.gridwidth

// should we add zeroline* attributes?
// might be useful on radial axes where range is negative and positive

// we could add spike* attributes down the road

This comment has been minimized.

Copy link
@alexcjohnson

alexcjohnson Dec 12, 2017

Contributor

This is another one that micropolar has (specifically in cursor-following mode ala #2155) that would be nice to preserve, since it's much harder to visually judge radius and angle than x and y. But we can leave that out of the initial PR if it goes in a "polar open items" issue.

zeroline* I'd leave out unless/until someone asks for it.

This comment has been minimized.

Copy link
@etpinard

etpinard Dec 21, 2017

Author Member

Good call about spike* attributes - though as a lot of spike-related logic will change during the push for #2155, I'll wait before implementing it if you don't mind.

}, 'plot', 'from-root');

var axisTickAttr = overrideAll({
tickmode: axesAttrs.tickmode,
nticks: axesAttrs.nticks,
tick0: axesAttrs.tick0,
dtick: axesAttrs.dtick,
tickvals: axesAttrs.tickvals,
ticktext: axesAttrs.ticktext,
ticks: axesAttrs.ticks,
ticklen: axesAttrs.ticklen,
tickwidth: axesAttrs.tickwidth,
tickcolor: axesAttrs.tickcolor,
showticklabels: axesAttrs.showticklabels,
showtickprefix: axesAttrs.showtickprefix,
tickprefix: axesAttrs.tickprefix,
showticksuffix: axesAttrs.showticksuffix,
ticksuffix: axesAttrs.ticksuffix,
showexponent: axesAttrs.showexponent,
exponentformat: axesAttrs.exponentformat,
separatethousands: axesAttrs.separatethousands,
tickfont: axesAttrs.tickfont,
tickangle: axesAttrs.tickangle,
tickformat: axesAttrs.tickformat,
tickformatstops: axesAttrs.tickformatstops,
}, 'plot', 'from-root');

var radialAxisAttrs = {
visible: extendFlat({}, axesAttrs.visible, {dflt: true}),
type: axesAttrs.type,

// You thought maybe that range should only be a 'max' instead
// as it always starts at 0? But, looks like off-zero cutout polar chart are
// a thing:
// -> mpl allow radial ranges to start off 0
// -> same for matlab: https://www.mathworks.com/help/matlab/ref/rlim.html
autorange: axesAttrs.autorange,
// might make 'nonnegative' the default,
// or add a special polar algo.

This comment has been minimized.

Copy link
@alexcjohnson

alexcjohnson Dec 12, 2017

Contributor

not tozero? There's also the question (mentioned in a private convo with @etpinard ) of what to do with negative values. If your plot represents a real physical space (see #2200 (comment) - but the clearest determinant of this may be "does r=0 correspond to a single point independent of theta, or does theta still have a meaning there?") then the center of the radial axis should be a non-editable r=0, and negative radius should (at least optionally) show up as a positive radius offset 180-degrees.

This comment has been minimized.

Copy link
@chriddyp

chriddyp Dec 12, 2017

Member

of what to do with negative values

The common case here would be plotting decibels (which is really just log data, therefore it has a negative) in e.g. antenna design
image

This comment has been minimized.

Copy link
@alexcjohnson

alexcjohnson Dec 12, 2017

Contributor

yeah so I guess what I really meant was "what do do with values below the minimum range value" - in your antenna example, you still don't want values below this limit (-10 here) to cross over to the other side, you want them to disappear.

This comment has been minimized.

Copy link
@etpinard

etpinard Dec 21, 2017

Author Member

Good point about r beyond radialaxis.range[0] - we'll need to add another attribute. As of 1e769f0 I simply skip over them.

We could either pick something granular like a boolean radialaxis.allowvaluesbeyondminrange or we could try to combine a few bits of behavior (like zoombox interactions) into a single attribute e.g. a boolean polar.independentaxes. Thoughts?

This comment has been minimized.

Copy link
@alexcjohnson

alexcjohnson Dec 21, 2017

Contributor

For the most part I think the two are the same thing: either you've mapped a real x/y onto r/theta, then you should not be able to set radialaxis.range[0] to anything other than 0, and negative radii should cross over to the opposite side; or angle and radius are just two aspects you're using to display data, in which case changing radialaxis.range[0] is meaningful, and crossing over is NOT meaningful.

That said I can imagine people wanting negative radii to be hidden even in the first case... but the last combination (variable radialaxis.range[0] but values beyond this crossing to the other side) would be really bizarre and I don't think we should allow it.

What about a new radialaxis.rangemode: 'fixedzero' value special for polar (which I'm tempted to say should be the default?), then a boolean that's only available in 'fixedzero' mode radialaxis.negativevaluescross, or perhaps an enum radialaxis.negativevalues: ('hide'|'cross')? Notice that if you show negative values it will impact the autorange algo, if there's a bigger negative than the biggest positive value.

This comment has been minimized.

Copy link
@etpinard

etpinard Jan 4, 2018

Author Member

rangemode: 'tozero' was made the default in 1e81039 - 'fixedzero'-type solution is sill an open item though.

rangemode: axesAttrs.rangemode,
range: axesAttrs.range,

categoryorder: axesAttrs.categoryorder,
categoryarray: axesAttrs.categoryarray,

// position (name analogous to xaxis.position),

This comment has been minimized.

Copy link
@cldougl

cldougl Dec 12, 2017

Member

I typically like keeping the attributes analogous across types but in this case I think I prefer: angleoffset

// or maybe something more specific e.g. angle angleoffset?
//
// (should this support any data coordinate system?)
// I think it is more intuitive to set this as just an angle!
// Thoughts?

This comment has been minimized.

Copy link
@alexcjohnson

alexcjohnson Dec 12, 2017

Contributor

I like angle for this. A side-effect is if you rotate the whole plot, the angular axis labels don't move, which I think is probably the desired effect.

Only case I see where this could get weird is square gridlines... maybe in that case we snap to the nearest category?

This comment has been minimized.

Copy link
@etpinard

etpinard Jan 4, 2018

Author Member

radialaxis.position became radialaxis.angle in 1e81039

position: {
valType: 'angle',
editType: 'plot',
role: 'info',
description: [
'Sets the angle (in degrees) from which the radial axis is drawn.',
'Note that by default, radial axis line on the theta=0 line',
'corresponds to a line pointing right (like what mathematicians prefer).',
'Defaults to the first `polar.sector` angle.'
].join(' ')
},

This comment has been minimized.

Copy link
@chriddyp

chriddyp Dec 12, 2017

Member

does it make sense to match pie here too, with rotation? https://plot.ly/python/reference/#pie-rotation

This comment has been minimized.

Copy link
@etpinard

etpinard Dec 21, 2017

Author Member

That's currently under angularaxis.position.

Originally, I wanted to keep some symmetry between radial and angular axes, so I made both axes have a position attribute. But now, that @cldougl and @alexcjohnson prefer radialaxis.angle(offset) over radialaxis.position, I'm thinking of renaming angularaxis.position -> angularaxis.rotation (or even polar.rotation) ? That said, rotation: 0 on pie traces corresponds to the 12 o'clock position whereas I chose the math-way (i.e. starting from 3 o'clock). @alexcjohnson in 3c8ae5b#r26220654 argued for 12 o'clock starting position, so yeah we could make pie and polar rotation essentially equivalent if we want to.


side: {
valType: 'enumerated',
// maybe 'clockwise' and 'counterclockwise' would be best here

This comment has been minimized.

Copy link
@alexcjohnson

alexcjohnson Dec 12, 2017

Contributor

👍 'clockwise' and 'counterclockwise'. Also if you set showline: false ala the births/hour plot, 'center' would be cool.

This comment has been minimized.

Copy link
@etpinard

etpinard Jan 4, 2018

Author Member

changed to 'clockwise' and 'counterclockwise' in 1e81039

values: ['left', 'right'],

This comment has been minimized.

Copy link
@chriddyp

chriddyp Dec 12, 2017

Member

does it make sense to match pie charts attributes like direction: 'clockwise' | 'counterclockwise'? https://plot.ly/python/reference/#pie-direction

This comment has been minimized.

Copy link
@etpinard

etpinard Dec 21, 2017

Author Member

That's under angularaxis.direction.

dflt: 'right',
editType: 'plot',
role: 'info',
description: [
'Determines on which side of radial axis line',
'the tick and tick labels appear.'
].join(' ')
},

// not sure about these
// maybe just for radialaxis ??
title: axesAttrs.title,
titlefont: axesAttrs.titlefont,

// only applies to radial axis for now (i.e. for cliponaxis: false traces)
// but angular.layer could be a thing later
layer: axesAttrs.layer,

hoverformat: axesAttrs.hoverformat,

// More attributes:

// We'll need some attribute that determines the span
// to draw donut-like charts
// e.g. https://github.com/matplotlib/matplotlib/issues/4217
//
// maybe something like 'span' or 'hole' (like pie, but pie set it in data coords?)
// span: {},
// hole: 1

This comment has been minimized.

Copy link
@alexcjohnson

alexcjohnson Dec 12, 2017

Contributor

I'd do hole, 0 to 1 as a fraction of the full radius, and not available in the real-space-mapping case.


// maybe should add a boolean to enable square grid lines
// and square axis lines
// (most common in radar-like charts)
// e.g. squareline/squaregrid or showline/showgrid: 'square' (on-top of true)

This comment has been minimized.

Copy link
@alexcjohnson

alexcjohnson Dec 12, 2017

Contributor

Yes, this affects radial gridlines, but I'd make it an attribute of the angular axis. Only available for category angular axes.


editType: 'calc'
};

extendFlat(
radialAxisAttrs,

// N.B. the radialaxis grid lines are circular,
// but radialaxis lines are straight from circle center to outer bound
axisLineGridAttr,
axisTickAttr
);

var angularAxisAttrs = {
visible: extendFlat({}, axesAttrs.visible, {dflt: true}),
type: {
valType: 'enumerated',
// 'linear' should maybe be called 'angle' or 'angular' here
// to make clear that axis here is periodic and more tightly match
// `thetaunit`?
//
// no 'log' for now
values: ['-', 'linear', 'date', 'category'],
dflt: '-',
role: 'info',
editType: 'calc',
description: [
'Sets the angular axis type.',
'If *linear*, set `thetaunit` to determine the unit in which axis value are shown.',
'If *date*, set `period` to determine the wrap around period.',

This comment has been minimized.

Copy link
@chriddyp

chriddyp Dec 12, 2017

Member

If date, use period to set the unit of time that determines a complete rotation

?

This comment has been minimized.

Copy link
@etpinard

etpinard Jan 4, 2018

Author Member

updated in 1e81039

'If *category, set `period` to determine the number of integer coordinates around polar axis.'
].join(' ')
},

categoryorder: axesAttrs.categoryorder,
categoryarray: axesAttrs.categoryarray,

thetaunit: {
valType: 'enumerated',
values: ['radians', 'degrees'],
dflt: 'degrees',
role: 'info',
editType: 'calc',
description: [
'Sets the format unit of the formatted *theta* values.',
'Has an effect only when `angularaxis.type` is *linear*.'
].join(' ')
},

period: {
valType: 'any',

This comment has been minimized.

Copy link
@alexcjohnson

alexcjohnson Dec 12, 2017

Contributor

'number'? Even for dates, this will be in milliseconds, right?

This comment has been minimized.

Copy link
@alexcjohnson

alexcjohnson Dec 12, 2017

Contributor

Hmm that raises a question though of how to deal with year-periodic data, as years aren't all the same length... I don't know what to do about that, just tell people to use 365.25 days? I wonder how your example plots handle this?

This comment has been minimized.

Copy link
@etpinard

etpinard Dec 21, 2017

Author Member

just tell people to use 365.25 days?

I'd guess they probably removed all February 29s from their time series.

This comment has been minimized.

Copy link
@etpinard

etpinard Dec 21, 2017

Author Member

Even for dates, this will be in milliseconds, right?

true, but perhaps we could allow the special dtick values as well e.g. M1, M3 and M12

This comment has been minimized.

Copy link
@alexcjohnson

alexcjohnson Dec 21, 2017

Contributor

It's one thing to use nonuniform quantities for dtick - ticks/grids are used to help you track data or delineate meaningful boundaries, and uniformity can help but is not always required.

But this is a period - if it's not exactly periodic we're going to be making all sorts of value judgments (along with probably complicated logic) somewhere. M1 would be an extreme example of this - it sounds good to want to look at the month-periodicity of some quantity, but what would we do with the data for months ranging from 28 to 31 days (not even going to think about world calendars 🙈 )? Stretch them to start and end at the same place? Leave the ends ragged, with gaps or long lines connecting to the next period? And if M2, M3, etc, do we do all of that month-by-month around the edge, so the first of the month always lines up?

I feel like it's much better to insist on exact periodicity - though if you want to alias M<n> to "the number of milliseconds in n/12 average years" to make it easier to use, I wouldn't be opposed. But I don't see a big disadvantage to having the angular points slightly misaligned from one period to the next, when the units we use to collect our data are not precisely periodic.

Then if someone wants one of those other interpretations (months stretched to all be the same length, or ragged ends, etc) they can massage their data to do any of those, with semi-fictitious angular data and the raw angular data in text or hovertext.

editType: 'calc',
role: 'info',
description: ''

// 360 / 2*pi for linear (might not need to set it)
// and to full range for other types

// 'period' is the angular equivalent to 'range'

// similar to dtick, one way to achieve e.g.:
// - period that equals the timeseries length
// http://flowingdata.com/2017/01/24/one-dataset-visualized-25-ways/18-polar-coordinates/
// - and 1-year periods (focusing on seasonal change0
// http://otexts.org/fpp2/seasonal-plots.html
// https://blogs.scientificamerican.com/sa-visual/why-are-so-many-babies-born-around-8-00-a-m/
// http://www.seasonaladjustment.com/2012/09/05/clock-plot-visualising-seasonality-using-r-and-ggplot2-part-3/
// https://i.pinimg.com/736x/49/b9/72/49b972ccb3206a1a6d6f870dac543280.jpg
// https://www.climate-lab-book.ac.uk/spirals/
},

direction: {
valType: 'enumerated',
values: ['counterclockwise', 'clockwise'],
// we could make the default 'clockwise' for date axes ...
dflt: 'counterclockwise',
role: 'info',
editType: 'calc',
description: [
'Sets the direction corresponding to positive angles.'
].join(' ')
},

// matlab uses thetaZeroLocation: 'North', 'West', 'East', 'South'
// mpl uses set_theta_zero_location('W', offset=10)
//
// position is analogous to yaxis.position, but as an angle (going
// counterclockwise about cartesian y=0.
position: {
valType: 'angle',
// we could maybe make `position: 90` by default for category and date angular axes.

This comment has been minimized.

Copy link
@alexcjohnson

alexcjohnson Dec 12, 2017

Contributor

I kind of think mathematicians should get relegated to non-default status here, and have the default start angle always be straight up (and define that as zero as it is for pies) as well as direction (default clockwise).

Thoughts?

This comment has been minimized.

Copy link
@etpinard

etpinard Dec 12, 2017

Author Member

I think the most way to the most universal. Maybe not the most useful, but most universal. So I'd vote for the math way, but I can be convinced otherwise.

This comment has been minimized.

Copy link
@alexcjohnson

alexcjohnson Dec 21, 2017

Contributor

I don't see anything "universal" about either one. There may be a common default among other packages, which is worth something, but absent that I feel like "up" is more meaningful to viewers than "right" is.

This comment has been minimized.

Copy link
@etpinard

etpinard Jan 4, 2018

Author Member

Both matlab and matplotlib use the math convention as their default. I would prefer going with that unless someone seriously objects it. Note that I'm open to defaulting angularaxis rotation to 90 and direction to 'clockwise' for categorical and dates angular axes.

This comment has been minimized.

Copy link
@alexcjohnson

alexcjohnson Jan 5, 2018

Contributor

dunno about saying "both" for matlab and the-package-porting-matlab-to-python 😈 but it does seem like that's the winning default other places too - origin, mathematica, maple... So I'll relent, even though I don't think that convention was ever designed, probably just grew accidentally from "we always draw the real number line horizontal" to "complex numbers make a plane! but keep the real line horizontal." to "hey look, an imaginary exponent is theta!"

Anyway as mentioned elsewhere I like the idea of clockwise defaulting to having zero at the top and CCW having it on the right, but not so much the idea of the default depending on axis type.

This comment has been minimized.

Copy link
@etpinard

etpinard Jan 5, 2018

Author Member

I like the idea of clockwise defaulting to having zero at the top and CCW having it on the right

This sounds like a nice compromise 👌

dflt: 0,
editType: 'calc',
role: 'info',
description: [
'Sets that start position (in degrees) of the angular axis',
'Note that by default, polar subplots are orientation such that the theta=0',
'corresponds to a line pointing right (like what mathematicians prefer).',
'For example to make the angular axis start from the North (like on a compass),',
'set `angularaxis.position` to *90*.'

This comment has been minimized.

Copy link
@chriddyp

chriddyp Dec 12, 2017

Member

very nice description!

].join(' ')
},

hoverformat: axesAttrs.hoverformat,

editType: 'calc'
};

extendFlat(
angularAxisAttrs,

// N.B. the angular grid lines are straight lines from circle center to outer bound
// the angular line is circular bounding the polar plot area.
axisLineGridAttr,
// Note that ticksuffix defaults to '°' for angular axes with `thetaunit: 'degrees'`
axisTickAttr
);

module.exports = {
// AJ and I first thought about a x/y/zoom system for paper-based zooming
// but I came to think that sector span + radial axis range
// zooming will be better
//
// TODO confirm with team.
// x: {},
// y: {},
// zoom: {},

domain: {
x: extendFlat({}, domainItem, {
description: [
'Sets the horizontal domain of this subplot',
'(in plot fraction).'
].join(' ')
}),
y: extendFlat({}, domainItem, {
description: [
'Sets the vertical domain of this subplot',
'(in plot fraction).'
].join(' ')
}),
editType: 'plot'
},

// Maybe this should angularaxis.range correspond to
// angular span of the drawing area?
//
// matlab's angular equivalent to 'range' bounds the drawing area
// (partial circles as they call it)
// https://www.mathworks.com/help/matlab/ref/thetalim.html
//
// as this attribute would be best set in (absolute) angles,
// I think this should be set outside of angularaxis e.g
// as polar.sector: [0, 180]
sector: {
valType: 'info_array',
items: [
// or be more strict -> `valType: 'angle' with `dflt: [0, 360]`
{valType: 'number', editType: 'plot'},
{valType: 'number', editType: 'plot'}
],
dflt: [0, 360],
role: 'info',
editType: 'plot',
description: [
'Sets angular span of this polar subplot with two angles (in degrees).',
'Sector are assumed to be spanned in the counterclockwise direction',
'with *0* corresponding to rightmost limit of the polar subplot.'
].join(' ')
},

bgcolor: {
valType: 'color',
role: 'style',
editType: 'plot',
dflt: colorAttrs.background,
description: 'Set the background color of the subplot'
},

radialaxis: radialAxisAttrs,
angularaxis: angularAxisAttrs,

// TODO maybe?
// annotations:

This comment has been minimized.

Copy link
@alexcjohnson

alexcjohnson Dec 12, 2017

Contributor

😍 but can be left to a "polar open items" issue

This comment has been minimized.

Copy link
@cldougl

cldougl Dec 12, 2017

Member

+💯


editType: 'calc'
};

0 comments on commit 3c8ae5b

Please sign in to comment.
You can’t perform that action at this time.