Skip to content

Commit f0ac957

Browse files
committed
implement uniformtext and insidetext-orientation for pie and sunburst
1 parent b234d7a commit f0ac957

File tree

10 files changed

+248
-116
lines changed

10 files changed

+248
-116
lines changed

src/traces/pie/attributes.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,21 @@ module.exports = {
181181
textfont: extendFlat({}, textFontAttrs, {
182182
description: 'Sets the font used for `textinfo`.'
183183
}),
184+
insidetextorientation: {
185+
valType: 'enumerated',
186+
role: 'info',
187+
values: ['h', 'r', 't', 'auto'],
188+
dflt: 'auto',
189+
editType: 'plot',
190+
description: [
191+
'Determines the orientation of text inside slices.',
192+
'With *auto* the texts may automatically be',
193+
'rotated to fit with the maximum size inside the slice.',
194+
'Using *h* option forces text to be horizontal.',
195+
'Using *r* option forces text to be radial.',
196+
'Using *t* option forces text to be tangential.'
197+
].join(' ')
198+
},
184199
insidetextfont: extendFlat({}, textFontAttrs, {
185200
description: 'Sets the font used for `textinfo` lying inside the sector.'
186201
}),

src/traces/pie/defaults.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
7070
if(hasOutside) {
7171
coerce('automargin');
7272
}
73+
74+
if(textposition !== 'none' && textposition !== 'outside') {
75+
coerce('insidetextorientation');
76+
}
7377
}
7478

7579
handleDomainDefaults(traceOut, layout, coerce);

src/traces/pie/plot.js

Lines changed: 168 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ var Color = require('../../components/color');
1616
var Drawing = require('../../components/drawing');
1717
var Lib = require('../../lib');
1818
var svgTextUtils = require('../../lib/svg_text_utils');
19+
var recordMinTextSize = require('../bar/plot').recordMinTextSize;
1920

2021
var helpers = require('./helpers');
2122
var eventData = require('./event_data');
@@ -52,7 +53,7 @@ function plot(gd, cdModule) {
5253
];
5354
var hasOutsideText = false;
5455

55-
slices.each(function(pt) {
56+
slices.each(function(pt, i) {
5657
if(pt.hidden) {
5758
d3.select(this).selectAll('path,g').remove();
5859
return;
@@ -131,7 +132,7 @@ function plot(gd, cdModule) {
131132
formatSliceLabel(gd, pt, cd0);
132133
var textPosition = helpers.castOption(trace.textposition, pt.pts);
133134
var sliceTextGroup = sliceTop.selectAll('g.slicetext')
134-
.data(pt.text && (textPosition !== 'none') ? [0] : []);
135+
.data(pt.text && (textPosition !== 'none') ? [pt] : []);
135136

136137
sliceTextGroup.enter().append('g')
137138
.classed('slicetext', true);
@@ -144,15 +145,19 @@ function plot(gd, cdModule) {
144145
s.attr('data-notex', 1);
145146
});
146147

148+
var font = Lib.extendFlat({}, textPosition === 'outside' ?
149+
determineOutsideTextFont(trace, pt, fullLayout.font) :
150+
determineInsideTextFont(trace, pt, fullLayout.font), {}
151+
);
152+
font.size = Math.max(font.size, fullLayout.uniformtext.minsize || 0);
153+
147154
sliceText.text(pt.text)
148155
.attr({
149156
'class': 'slicetext',
150157
transform: '',
151158
'text-anchor': 'middle'
152159
})
153-
.call(Drawing.font, textPosition === 'outside' ?
154-
determineOutsideTextFont(trace, pt, gd._fullLayout.font) :
155-
determineInsideTextFont(trace, pt, gd._fullLayout.font))
160+
.call(Drawing.font, font)
156161
.call(svgTextUtils.convertToTspans, gd);
157162

158163
// position the text relative to the slice
@@ -164,36 +169,39 @@ function plot(gd, cdModule) {
164169
} else {
165170
transform = transformInsideText(textBB, pt, cd0);
166171
if(textPosition === 'auto' && transform.scale < 1) {
167-
sliceText.call(Drawing.font, trace.outsidetextfont);
168-
if(trace.outsidetextfont.family !== trace.insidetextfont.family ||
169-
trace.outsidetextfont.size !== trace.insidetextfont.size) {
172+
var newFont = Lib.extendFlat({}, trace.outsidetextfont, {});
173+
newFont.size = Math.max(newFont.size, fullLayout.uniformtext.minsize || 0);
174+
175+
sliceText.call(Drawing.font, newFont);
176+
if(newFont.family !== font.family || newFont.size !== font.size) {
177+
// recompute bounding box
170178
textBB = Drawing.bBox(sliceText.node());
171179
}
172180
transform = transformOutsideText(textBB, pt);
173181
}
174182
}
175183

176-
var translateX = cx + pt.pxmid[0] * transform.rCenter + (transform.x || 0);
177-
var translateY = cy + pt.pxmid[1] * transform.rCenter + (transform.y || 0);
184+
var pxtxt = pt.pxtxt || pt.pxmid;
185+
transform.targetX = cx + pxtxt[0] * transform.rCenter + (transform.x || 0);
186+
transform.targetY = cy + pxtxt[1] * transform.rCenter + (transform.y || 0);
187+
computeTransform(transform, textBB);
178188

179189
// save some stuff to use later ensure no labels overlap
180190
if(transform.outside) {
181-
pt.yLabelMin = translateY - textBB.height / 2;
182-
pt.yLabelMid = translateY;
183-
pt.yLabelMax = translateY + textBB.height / 2;
191+
var targetY = transform.targetY;
192+
pt.yLabelMin = targetY - textBB.height / 2;
193+
pt.yLabelMid = targetY;
194+
pt.yLabelMax = targetY + textBB.height / 2;
184195
pt.labelExtraX = 0;
185196
pt.labelExtraY = 0;
186197
hasOutsideText = true;
187198
}
188199

189-
sliceText.attr('transform',
190-
'translate(' + translateX + ',' + translateY + ')' +
191-
(transform.scale < 1 ? ('scale(' + transform.scale + ')') : '') +
192-
(transform.rotate ? ('rotate(' + transform.rotate + ')') : '') +
193-
'translate(' +
194-
(-(textBB.left + textBB.right) / 2) + ',' +
195-
(-(textBB.top + textBB.bottom) / 2) +
196-
')');
200+
transform.fontSize = font.size;
201+
recordMinTextSize(trace.type, transform, fullLayout);
202+
cd[i].transform = transform;
203+
204+
sliceText.attr('transform', Lib.getTextTransform(transform, true));
197205
});
198206
});
199207

@@ -298,8 +306,10 @@ function plotTextLines(slices, trace) {
298306
// first move the text to its new location
299307
var sliceText = sliceTop.select('g.slicetext text');
300308

301-
sliceText.attr('transform', 'translate(' + pt.labelExtraX + ',' + pt.labelExtraY + ')' +
302-
sliceText.attr('transform'));
309+
pt.transform.targetX += pt.labelExtraX;
310+
pt.transform.targetY += pt.labelExtraY;
311+
312+
sliceText.attr('transform', Lib.getTextTransform(pt.transform));
303313

304314
// then add a line to the new location
305315
var lineStartX = pt.cxFinal + pt.pxmid[0];
@@ -549,59 +559,126 @@ function prerenderTitles(cdModule, gd) {
549559

550560
function transformInsideText(textBB, pt, cd0) {
551561
var textDiameter = Math.sqrt(textBB.width * textBB.width + textBB.height * textBB.height);
552-
var textAspect = textBB.width / textBB.height;
553562
var halfAngle = pt.halfangle;
563+
var midAngle = pt.midangle;
554564
var ring = pt.ring;
555565
var rInscribed = pt.rInscribed;
556566
var r = cd0.r || pt.rpx1;
567+
var orientation = cd0.trace.insidetextorientation;
568+
var allTransforms = [];
569+
570+
var isCircle = (ring === 1) && (Math.abs(pt.startangle - pt.stopangle) === Math.PI * 2);
571+
572+
if(isCircle || orientation === 'auto' || orientation === 'h') {
573+
// max size text can be inserted inside without rotating it
574+
// this inscribes the text rectangle in a circle, which is then inscribed
575+
// in the slice, so it will be an underestimate, which some day we may want
576+
// to improve so this case can get more use
577+
var transform = {
578+
scale: rInscribed * r * 2 / textDiameter,
579+
580+
// and the center position and rotation in this case
581+
rCenter: 1 - rInscribed,
582+
rotate: 0
583+
};
557584

558-
// max size text can be inserted inside without rotating it
559-
// this inscribes the text rectangle in a circle, which is then inscribed
560-
// in the slice, so it will be an underestimate, which some day we may want
561-
// to improve so this case can get more use
562-
var transform = {
563-
scale: rInscribed * r * 2 / textDiameter,
585+
if(transform.scale >= 1) return transform;
564586

565-
// and the center position and rotation in this case
566-
rCenter: 1 - rInscribed,
567-
rotate: 0
568-
};
587+
allTransforms.push(transform);
588+
}
569589

570-
if(transform.scale >= 1) return transform;
590+
if(orientation === 'h') {
591+
// max size if text is placed (horizontally) at the top or bottom of the arc
571592

572-
// max size if text is rotated radially
573-
var Qr = textAspect + 1 / (2 * Math.tan(halfAngle));
574-
var maxHalfHeightRotRadial = r * Math.min(
575-
1 / (Math.sqrt(Qr * Qr + 0.5) + Qr),
576-
ring / (Math.sqrt(textAspect * textAspect + ring / 2) + textAspect)
593+
var considerCrossing = function(angle, key) {
594+
if(isCrossing(pt, angle)) {
595+
var dStart = Math.abs(angle - pt.startangle);
596+
var dStop = Math.abs(angle - pt.stopangle);
597+
598+
var closestEdge = dStart < dStop ? dStart : dStop;
599+
600+
var newT;
601+
if(key === 'tan') {
602+
newT = calcTanTransform(textBB, r, ring, closestEdge, 0);
603+
} else { // case of 'rad'
604+
newT = calcRadTransform(textBB, r, ring, closestEdge, Math.PI / 2);
605+
}
606+
newT._repos = getCoords(r, angle);
607+
608+
allTransforms.push(newT);
609+
}
610+
};
611+
612+
for(var i = 3; i >= -3; i--) { // to cover all cases with trace.rotation added
613+
considerCrossing(Math.PI * (i + 0.0), 'tan');
614+
considerCrossing(Math.PI * (i + 0.5), 'rad');
615+
}
616+
}
617+
618+
if(orientation === 'auto' || orientation === 'r') {
619+
allTransforms.push(calcRadTransform(textBB, r, ring, halfAngle, midAngle));
620+
}
621+
622+
if(orientation === 'auto' || orientation === 't') {
623+
allTransforms.push(calcTanTransform(textBB, r, ring, halfAngle, midAngle));
624+
}
625+
626+
var maxScaleTransform = allTransforms.sort(function(a, b) {
627+
return b.scale - a.scale;
628+
})[0];
629+
630+
if(maxScaleTransform._repos) {
631+
pt.pxtxt = maxScaleTransform._repos;
632+
}
633+
634+
return maxScaleTransform;
635+
}
636+
637+
function isCrossing(pt, angle) {
638+
var start = pt.startangle;
639+
var stop = pt.stopangle;
640+
return (
641+
(start > angle && angle > stop) ||
642+
(start < angle && angle < stop)
577643
);
578-
var radialTransform = {
579-
scale: maxHalfHeightRotRadial * 2 / textBB.height,
580-
rCenter: Math.cos(maxHalfHeightRotRadial / r) -
581-
maxHalfHeightRotRadial * textAspect / r,
582-
rotate: (180 / Math.PI * pt.midangle + 720) % 180 - 90
644+
}
645+
646+
function calcRadTransform(textBB, r, ring, halfAngle, midAngle) {
647+
// max size if text is rotated radially
648+
var a = textBB.width / textBB.height;
649+
var s = calcMaxHalfSize(a, halfAngle, r, ring);
650+
return {
651+
scale: s * 2 / textBB.height,
652+
rCenter: calcRCenter(a, s / r),
653+
rotate: calcRotate(midAngle)
583654
};
655+
}
584656

657+
function calcTanTransform(textBB, r, ring, halfAngle, midAngle) {
585658
// max size if text is rotated tangentially
586-
var aspectInv = 1 / textAspect;
587-
var Qt = aspectInv + 1 / (2 * Math.tan(halfAngle));
588-
var maxHalfWidthTangential = r * Math.min(
589-
1 / (Math.sqrt(Qt * Qt + 0.5) + Qt),
590-
ring / (Math.sqrt(aspectInv * aspectInv + ring / 2) + aspectInv)
591-
);
592-
var tangentialTransform = {
593-
scale: maxHalfWidthTangential * 2 / textBB.width,
594-
rCenter: Math.cos(maxHalfWidthTangential / r) -
595-
maxHalfWidthTangential / textAspect / r,
596-
rotate: (180 / Math.PI * pt.midangle + 810) % 180 - 90
659+
var a = textBB.height / textBB.width;
660+
var s = calcMaxHalfSize(a, halfAngle, r, ring);
661+
return {
662+
scale: s * 2 / textBB.width,
663+
rCenter: calcRCenter(a, s / r),
664+
rotate: calcRotate(midAngle + Math.PI / 2)
597665
};
598-
// if we need a rotated transform, pick the biggest one
599-
// even if both are bigger than 1
600-
var rotatedTransform = tangentialTransform.scale > radialTransform.scale ?
601-
tangentialTransform : radialTransform;
666+
}
667+
668+
function calcRCenter(a, b) {
669+
return Math.cos(b) - a * b;
670+
}
671+
672+
function calcRotate(t) {
673+
return (180 / Math.PI * t + 720) % 180 - 90;
674+
}
602675

603-
if(transform.scale < 1 && rotatedTransform.scale > transform.scale) return rotatedTransform;
604-
return transform;
676+
function calcMaxHalfSize(a, halfAngle, r, ring) {
677+
var q = a + 1 / (2 * Math.tan(halfAngle));
678+
return r * Math.min(
679+
1 / (Math.sqrt(q * q + 0.5) + q),
680+
ring / (Math.sqrt(a * a + ring / 2) + a)
681+
);
605682
}
606683

607684
function getInscribedRadiusFraction(pt, cd0) {
@@ -921,6 +998,7 @@ function groupScale(cdModule, scaleGroups) {
921998

922999
function setCoords(cd) {
9231000
var cd0 = cd[0];
1001+
var r = cd0.r;
9241002
var trace = cd0.trace;
9251003
var currentAngle = trace.rotation * Math.PI / 180;
9261004
var angleFactor = 2 * Math.PI / cd0.vTotal;
@@ -941,24 +1019,21 @@ function setCoords(cd) {
9411019
lastPt = 'px0';
9421020
}
9431021

944-
function getCoords(angle) {
945-
return [cd0.r * Math.sin(angle), -cd0.r * Math.cos(angle)];
946-
}
947-
948-
currentCoords = getCoords(currentAngle);
1022+
currentCoords = getCoords(r, currentAngle);
9491023

9501024
for(i = 0; i < cd.length; i++) {
9511025
cdi = cd[i];
9521026
if(cdi.hidden) continue;
9531027

9541028
cdi[firstPt] = currentCoords;
9551029

1030+
cdi.startangle = currentAngle;
9561031
currentAngle += angleFactor * cdi.v / 2;
957-
cdi.pxmid = getCoords(currentAngle);
1032+
cdi.pxmid = getCoords(r, currentAngle);
9581033
cdi.midangle = currentAngle;
959-
9601034
currentAngle += angleFactor * cdi.v / 2;
961-
currentCoords = getCoords(currentAngle);
1035+
currentCoords = getCoords(r, currentAngle);
1036+
cdi.stopangle = currentAngle;
9621037

9631038
cdi[lastPt] = currentCoords;
9641039

@@ -970,6 +1045,10 @@ function setCoords(cd) {
9701045
}
9711046
}
9721047

1048+
function getCoords(r, angle) {
1049+
return [r * Math.sin(angle), -r * Math.cos(angle)];
1050+
}
1051+
9731052
function formatSliceLabel(gd, pt, cd0) {
9741053
var fullLayout = gd._fullLayout;
9751054
var trace = cd0.trace;
@@ -1024,6 +1103,24 @@ function formatSliceLabel(gd, pt, cd0) {
10241103
}
10251104
}
10261105
}
1106+
1107+
function computeTransform(
1108+
transform, // inout
1109+
textBB // in
1110+
) {
1111+
var rotate = transform.rotate;
1112+
var scale = transform.scale;
1113+
if(scale > 1) scale = 1;
1114+
1115+
var a = rotate * Math.PI / 180;
1116+
var cosA = Math.cos(a);
1117+
var sinA = Math.sin(a);
1118+
var midX = (textBB.left + textBB.right) / 2;
1119+
var midY = (textBB.top + textBB.bottom) / 2;
1120+
transform.textX = midX * cosA - midY * sinA;
1121+
transform.textY = midX * sinA + midY * cosA;
1122+
}
1123+
10271124
module.exports = {
10281125
plot: plot,
10291126
formatSliceLabel: formatSliceLabel,
@@ -1033,4 +1130,5 @@ module.exports = {
10331130
prerenderTitles: prerenderTitles,
10341131
layoutAreas: layoutAreas,
10351132
attachFxHandlers: attachFxHandlers,
1133+
computeTransform: computeTransform
10361134
};

0 commit comments

Comments
 (0)