Skip to content

Commit ea7fd2c

Browse files
committed
add polar.hole
- draw subplot with pathAnnulus when polar.hole > 0 - update r range -> x/y range scales - draw angular grid line from innerRadius w/o translating them - constrain handles between innerRadius & radius on main drag - add mock
1 parent 39b2ee3 commit ea7fd2c

File tree

6 files changed

+137
-39
lines changed

6 files changed

+137
-39
lines changed

src/plots/polar/layout_attributes.js

+11-10
Original file line numberDiff line numberDiff line change
@@ -112,16 +112,6 @@ var radialAxisAttrs = {
112112

113113
hoverformat: axesAttrs.hoverformat,
114114

115-
// More attributes:
116-
117-
// We'll need some attribute that determines the span
118-
// to draw donut-like charts
119-
// e.g. https://github.com/matplotlib/matplotlib/issues/4217
120-
//
121-
// maybe something like 'span' or 'hole' (like pie, but pie set it in data coords?)
122-
// span: {},
123-
// hole: 1
124-
125115
editType: 'calc'
126116
};
127117

@@ -256,6 +246,17 @@ module.exports = {
256246
'with *0* corresponding to rightmost limit of the polar subplot.'
257247
].join(' ')
258248
},
249+
hole: {
250+
valType: 'number',
251+
min: 0,
252+
max: 1,
253+
dflt: 0,
254+
editType: 'plot',
255+
role: 'info',
256+
description: [
257+
'Sets the fraction of the radius to cut out of the polar subplot.'
258+
].join(' ')
259+
},
259260

260261
bgcolor: {
261262
valType: 'color',

src/plots/polar/layout_defaults.js

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ function handleDefaults(contIn, contOut, coerce, opts) {
3030
opts.bgColor = Color.combine(bgColor, opts.paper_bgcolor);
3131

3232
var sector = coerce('sector');
33+
coerce('hole');
3334

3435
// could optimize, subplotData is not always needed!
3536
var subplotData = getSubplotData(opts.fullData, constants.name, opts.id);

src/plots/polar/polar.js

+37-25
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,8 @@ proto.updateLayout = function(fullLayout, polarLayout) {
234234
var yOffset2 = _this.yOffset2 = gs.t + gs.h * (1 - yDomain2[1]);
235235
// circle radius in px
236236
var radius = _this.radius = xLength2 / dxSectorBBox;
237+
// 'inner' radius in px (when polar.hole is set)
238+
var innerRadius = _this.innerRadius = polarLayout.hole * radius;
237239
// circle center position in px
238240
var cx = _this.cx = xOffset2 - radius * sectorBBox[0];
239241
var cy = _this.cy = yOffset2 + radius * sectorBBox[3];
@@ -252,7 +254,7 @@ proto.updateLayout = function(fullLayout, polarLayout) {
252254
clockwise: 'bottom'
253255
}[radialLayout.side],
254256
// spans length 1 radius
255-
domain: [0, radius / gs.w]
257+
domain: [innerRadius / gs.w, radius / gs.w]
256258
});
257259

258260
_this.angularAxis = _this.mockAxis(fullLayout, polarLayout, angularLayout, {
@@ -282,7 +284,7 @@ proto.updateLayout = function(fullLayout, polarLayout) {
282284
domain: yDomain2
283285
});
284286

285-
var dPath = _this.pathSector();
287+
var dPath = _this.pathSubplot();
286288

287289
_this.clipPaths.forTraces.select('path')
288290
.attr('d', dPath)
@@ -333,9 +335,11 @@ proto.mockCartesianAxis = function(fullLayout, polarLayout, opts) {
333335

334336
ax.setRange = function() {
335337
var sectorBBox = _this.sectorBBox;
336-
var rl = _this.radialAxis._rl;
337-
var drl = rl[1] - rl[0];
338338
var ind = bboxIndices[axId];
339+
var rl = _this.radialAxis._rl;
340+
var radius = _this.radius;
341+
var innerRadius = _this.innerRadius;
342+
var drl = radius * (rl[1] - rl[0]) / (radius - innerRadius);
339343
ax.range = [sectorBBox[ind[0]] * drl, sectorBBox[ind[1]] * drl];
340344
};
341345

@@ -371,6 +375,7 @@ proto.updateRadialAxis = function(fullLayout, polarLayout) {
371375
var gd = _this.gd;
372376
var layers = _this.layers;
373377
var radius = _this.radius;
378+
var innerRadius = _this.innerRadius;
374379
var cx = _this.cx;
375380
var cy = _this.cy;
376381
var radialLayout = polarLayout.radialaxis;
@@ -392,12 +397,12 @@ proto.updateRadialAxis = function(fullLayout, polarLayout) {
392397

393398
// easier to set rotate angle with custom translate function
394399
ax._transfn = function(d) {
395-
return 'translate(' + ax.l2p(d.x) + ',0)';
400+
return 'translate(' + (ax.l2p(d.x) + innerRadius) + ',0)';
396401
};
397402

398403
// set special grid path function
399404
ax._gridpath = function(d) {
400-
return _this.pathArc(ax.r2p(d.x));
405+
return _this.pathArc(ax.r2p(d.x) + innerRadius);
401406
};
402407

403408
var newTickLayout = strTickLayout(radialLayout);
@@ -428,7 +433,7 @@ proto.updateRadialAxis = function(fullLayout, polarLayout) {
428433
.selectAll('path').attr('transform', null);
429434

430435
updateElement(layers['radial-line'].select('line'), radialLayout.showline, {
431-
x1: 0,
436+
x1: innerRadius,
432437
y1: 0,
433438
x2: radius,
434439
y2: 0,
@@ -479,6 +484,7 @@ proto.updateAngularAxis = function(fullLayout, polarLayout) {
479484
var gd = _this.gd;
480485
var layers = _this.layers;
481486
var radius = _this.radius;
487+
var innerRadius = _this.innerRadius;
482488
var cx = _this.cx;
483489
var cy = _this.cy;
484490
var angularLayout = polarLayout.angularaxis;
@@ -491,11 +497,6 @@ proto.updateAngularAxis = function(fullLayout, polarLayout) {
491497
// 't'ick to 'g'eometric radians is used all over the place here
492498
var t2g = function(d) { return ax.t2g(d.x); };
493499

494-
// (x,y) at max radius
495-
function rad2xy(rad) {
496-
return [radius * Math.cos(rad), radius * Math.sin(rad)];
497-
}
498-
499500
// run rad2deg on tick0 and ditck for thetaunit: 'radians' axes
500501
if(ax.type === 'linear' && ax.thetaunit === 'radians') {
501502
ax.tick0 = rad2deg(ax.tick0);
@@ -512,13 +513,17 @@ proto.updateAngularAxis = function(fullLayout, polarLayout) {
512513
}
513514

514515
ax._transfn = function(d) {
516+
var sel = d3.select(this);
517+
var hasElement = sel && sel.node();
518+
519+
// don't translate grid lines
520+
if(hasElement && sel.classed('angularaxisgrid')) return '';
521+
515522
var rad = t2g(d);
516-
var xy = rad2xy(rad);
517-
var out = strTranslate(cx + xy[0], cy - xy[1]);
523+
var out = strTranslate(cx + radius * Math.cos(rad), cy - radius * Math.sin(rad));
518524

519-
// must also rotate ticks, but don't rotate labels and grid lines
520-
var sel = d3.select(this);
521-
if(sel && sel.node() && sel.classed('ticks')) {
525+
// must also rotate ticks, but don't rotate labels
526+
if(hasElement && sel.classed('ticks')) {
522527
out += strRotate(-rad2deg(rad));
523528
}
524529

@@ -527,8 +532,10 @@ proto.updateAngularAxis = function(fullLayout, polarLayout) {
527532

528533
ax._gridpath = function(d) {
529534
var rad = t2g(d);
530-
var xy = rad2xy(rad);
531-
return 'M0,0L' + (-xy[0]) + ',' + xy[1];
535+
var cosRad = Math.cos(rad);
536+
var sinRad = Math.sin(rad);
537+
return 'M' + [cx + innerRadius * cosRad, cy - innerRadius * sinRad] +
538+
'L' + [cx + radius * cosRad, cy - radius * sinRad];
532539
};
533540

534541
var offset4fontsize = (angularLayout.ticks !== 'outside' ? 0.7 : 0.5);
@@ -591,7 +598,7 @@ proto.updateAngularAxis = function(fullLayout, polarLayout) {
591598
_this.vangles = vangles;
592599

593600
updateElement(layers['angular-line'].select('path'), angularLayout.showline, {
594-
d: _this.pathSector(),
601+
d: _this.pathSubplot(),
595602
transform: strTranslate(cx, cy)
596603
})
597604
.attr('stroke-width', angularLayout.linewidth)
@@ -614,6 +621,7 @@ proto.updateMainDrag = function(fullLayout) {
614621
var MINZOOM = constants.MINZOOM;
615622
var OFFEDGE = constants.OFFEDGE;
616623
var radius = _this.radius;
624+
var innerRadius = _this.innerRadius;
617625
var cx = _this.cx;
618626
var cy = _this.cy;
619627
var cxx = _this.cxx;
@@ -629,7 +637,7 @@ proto.updateMainDrag = function(fullLayout) {
629637
var mainDrag = dragBox.makeDragger(layers, 'path', 'maindrag', 'crosshair');
630638

631639
d3.select(mainDrag)
632-
.attr('d', _this.pathSector())
640+
.attr('d', _this.pathSubplot())
633641
.attr('transform', strTranslate(cx, cy));
634642

635643
var dragOpts = {
@@ -727,7 +735,7 @@ proto.updateMainDrag = function(fullLayout) {
727735
function zoomPrep() {
728736
r0 = null;
729737
r1 = null;
730-
path0 = _this.pathSector();
738+
path0 = _this.pathSubplot();
731739
dimmed = false;
732740

733741
var polarLayoutNow = gd._fullLayout[_this.id];
@@ -742,7 +750,7 @@ proto.updateMainDrag = function(fullLayout) {
742750
// N.B. this sets scoped 'r0' and 'r1'
743751
// return true if 'valid' zoom distance, false otherwise
744752
function clampAndSetR0R1(rr0, rr1) {
745-
rr1 = Math.min(rr1, radius);
753+
rr1 = Math.max(Math.min(rr1, radius), innerRadius);
746754

747755
// starting or ending drag near center (outer edge),
748756
// clamps radial distance at origin (at r=radius)
@@ -1193,15 +1201,13 @@ proto.isPtInside = function(d) {
11931201
};
11941202

11951203
proto.pathArc = function(r) {
1196-
r = r || this.radius;
11971204
var sectorInRad = this.sectorInRad;
11981205
var vangles = this.vangles;
11991206
var fn = vangles ? helpers.pathPolygon : Lib.pathArc;
12001207
return fn(r, sectorInRad[0], sectorInRad[1], vangles);
12011208
};
12021209

12031210
proto.pathSector = function(r) {
1204-
r = r || this.radius;
12051211
var sectorInRad = this.sectorInRad;
12061212
var vangles = this.vangles;
12071213
var fn = vangles ? helpers.pathPolygon : Lib.pathSector;
@@ -1215,6 +1221,12 @@ proto.pathAnnulus = function(r0, r1) {
12151221
return fn(r0, r1, sectorInRad[0], sectorInRad[1], vangles);
12161222
};
12171223

1224+
proto.pathSubplot = function() {
1225+
var r0 = this.innerRadius;
1226+
var r1 = this.radius;
1227+
return r0 ? this.pathAnnulus(r0, r1) : this.pathSector(r1);
1228+
};
1229+
12181230
proto.fillViewInitialKey = function(key, val) {
12191231
if(!(key in this.viewInitial)) {
12201232
this.viewInitial[key] = val;

src/plots/polar/set_convert.js

+8-4
Original file line numberDiff line numberDiff line change
@@ -61,25 +61,29 @@ module.exports = function setConvert(ax, polarLayout, fullLayout) {
6161
};
6262

6363
function setConvertRadial(ax, polarLayout) {
64+
var subplot = polarLayout._subplot;
65+
6466
ax.setGeometry = function() {
6567
var rl0 = ax._rl[0];
6668
var rl1 = ax._rl[1];
6769

70+
var b = subplot.innerRadius;
71+
var m = (subplot.radius - b) / (rl1 - rl0);
72+
var b2 = b / m;
73+
6874
var rFilter = rl0 > rl1 ?
6975
function(v) { return v <= 0; } :
7076
function(v) { return v >= 0; };
7177

7278
ax.c2g = function(v) {
7379
var r = ax.c2l(v) - rl0;
74-
return rFilter(r) ? r : 0;
80+
return (rFilter(r) ? r : 0) + b2;
7581
};
7682

7783
ax.g2c = function(v) {
78-
return ax.l2c(v + rl0);
84+
return ax.l2c(v + rl0 - b2);
7985
};
8086

81-
var m = polarLayout._subplot.radius / (rl1 - rl0);
82-
8387
ax.g2p = function(v) { return v * m; };
8488
ax.c2p = function(v) { return ax.g2p(ax.c2g(v)); };
8589
};

test/image/baselines/polar_hole.png

105 KB
Loading

test/image/mocks/polar_hole.json

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
{
2+
"data": [
3+
{
4+
"type": "barpolar",
5+
"r": [10, 12, 15]
6+
},
7+
{
8+
"type": "scatterpolar",
9+
"r": [10, 12, 15],
10+
"theta0": 90
11+
},
12+
13+
{
14+
"type": "scatterpolar",
15+
"subplot": "polar2",
16+
"r": [100, 50, 200]
17+
},
18+
{
19+
"type": "barpolar",
20+
"subplot": "polar2",
21+
"r": [100, 50, 200]
22+
},
23+
24+
{
25+
"type": "barpolar",
26+
"subplot": "polar3",
27+
"theta": ["a", "b", "c", "d", "b", "f", "a", "a"]
28+
},
29+
{
30+
"type": "scatterpolar",
31+
"subplot": "polar3",
32+
"theta": ["a", "b", "c", "d", "b", "f", "a", "a"]
33+
},
34+
35+
{
36+
"type": "barpolar",
37+
"subplot": "polar4",
38+
"r": [10, 12, 15]
39+
},
40+
{
41+
"type": "scatterpolar",
42+
"subplot": "polar4",
43+
"r": [10, 12, 15],
44+
"theta0": 90
45+
}
46+
],
47+
"layout": {
48+
"width": 600,
49+
"height": 600,
50+
"margin": {"l": 40, "r": 40, "b": 40, "t": 40, "pad": 0},
51+
"grid": {
52+
"rows": 2,
53+
"columns": 2,
54+
"ygap": 0.2
55+
},
56+
"polar": {
57+
"hole": 0.1,
58+
"domain": {"row": 0, "column": 0}
59+
},
60+
"polar2": {
61+
"hole": 0.4,
62+
"domain": {"row": 0, "column": 1},
63+
"radialaxis": {"type": "log"}
64+
},
65+
"polar3": {
66+
"hole": 0.2,
67+
"gridshape": "linear",
68+
"angularaxis": {"direction": "clockwise"},
69+
"domain": {"row": 1, "column": 0}
70+
},
71+
"polar4": {
72+
"hole": 0.4,
73+
"domain": {"row": 1, "column": 1},
74+
"sector": [0, 180],
75+
"angularaxis": {"direction": "clockwise"},
76+
"radialaxis": {"angle": 90, "side": "counterclockwise"}
77+
},
78+
"showlegend": false
79+
}
80+
}

0 commit comments

Comments
 (0)