Skip to content
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

Consistent text mode for bar-like & pie-like traces and feature to control text orientation inside pie/sunburst slices #4420

Merged
merged 20 commits into from
Dec 23, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0d3e289
add uniformtext attributes to layout
archmoj Dec 12, 2019
c095271
simplify transform function and expand to reuse in pie and sunburst
archmoj Dec 12, 2019
b234d7a
implement uniformtext for bar-like traces as well as funnelarea and t…
archmoj Dec 12, 2019
f0ac957
implement uniformtext and insidetext-orientation for pie and sunburst
archmoj Dec 12, 2019
4d5fb3d
add new mocks to test insidetextorientation and uniformtext
archmoj Dec 12, 2019
a27dc29
Consider first review
archmoj Dec 13, 2019
1479943
make tangential go towards noon and 6 and make radial go towards 3 and 9
archmoj Dec 16, 2019
eab205a
rewrite uniform text style
archmoj Dec 16, 2019
5348c58
add new mocks cases by Nicolas
archmoj Dec 18, 2019
15ce24d
apply zero scale for hiding text elements
archmoj Dec 19, 2019
0840d82
add new mocks using both inside-text-orientation and uniformtext
archmoj Dec 19, 2019
328ffd0
move reposition logic outside pie transformInsideText function
archmoj Dec 19, 2019
2a31685
apply next text position when it is not at the center
archmoj Dec 19, 2019
d1b50b1
Apply polar coordinates to move text inside the slice during sunburst…
archmoj Dec 19, 2019
2cb5687
sunburst handle no text when using various inside-text-orientation op…
archmoj Dec 20, 2019
beed042
add react tests for treemap and pie uniform text as well as inside te…
archmoj Dec 20, 2019
51d7e1b
add tests for updating text positions using different inside-text-pos…
archmoj Dec 20, 2019
603e0cc
wip
etpinard Dec 20, 2019
8784274
first attempt at fixing tests on etpinard's laptop
etpinard Dec 23, 2019
2686c46
Fix funnelarea, pie, sunburst and treemap tests
archmoj Dec 23, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/traces/pie/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,21 @@ module.exports = {
textfont: extendFlat({}, textFontAttrs, {
description: 'Sets the font used for `textinfo`.'
}),
insidetextorientation: {
valType: 'enumerated',
role: 'info',
values: ['h', 'r', 't', 'auto'],
etpinard marked this conversation as resolved.
Show resolved Hide resolved
dflt: 'auto',
editType: 'plot',
description: [
'Determines the orientation of text inside slices.',
'With *auto* the texts may automatically be',
'rotated to fit with the maximum size inside the slice.',
'Using *h* option forces text to be horizontal.',
'Using *r* option forces text to be radial.',
'Using *t* option forces text to be tangential.'
].join(' ')
},
insidetextfont: extendFlat({}, textFontAttrs, {
description: 'Sets the font used for `textinfo` lying inside the sector.'
}),
Expand Down
4 changes: 4 additions & 0 deletions src/traces/pie/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
if(hasOutside) {
coerce('automargin');
}

if(textposition !== 'none' && textposition !== 'outside') {
etpinard marked this conversation as resolved.
Show resolved Hide resolved
coerce('insidetextorientation');
}
}

handleDomainDefaults(traceOut, layout, coerce);
Expand Down
238 changes: 168 additions & 70 deletions src/traces/pie/plot.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var Color = require('../../components/color');
var Drawing = require('../../components/drawing');
var Lib = require('../../lib');
var svgTextUtils = require('../../lib/svg_text_utils');
var recordMinTextSize = require('../bar/plot').recordMinTextSize;

var helpers = require('./helpers');
var eventData = require('./event_data');
Expand Down Expand Up @@ -52,7 +53,7 @@ function plot(gd, cdModule) {
];
var hasOutsideText = false;

slices.each(function(pt) {
slices.each(function(pt, i) {
if(pt.hidden) {
d3.select(this).selectAll('path,g').remove();
return;
Expand Down Expand Up @@ -131,7 +132,7 @@ function plot(gd, cdModule) {
formatSliceLabel(gd, pt, cd0);
var textPosition = helpers.castOption(trace.textposition, pt.pts);
var sliceTextGroup = sliceTop.selectAll('g.slicetext')
.data(pt.text && (textPosition !== 'none') ? [0] : []);
.data(pt.text && (textPosition !== 'none') ? [pt] : []);

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

var font = Lib.extendFlat({}, textPosition === 'outside' ?
determineOutsideTextFont(trace, pt, fullLayout.font) :
determineInsideTextFont(trace, pt, fullLayout.font), {}
);
etpinard marked this conversation as resolved.
Show resolved Hide resolved
font.size = Math.max(font.size, fullLayout.uniformtext.minsize || 0);

sliceText.text(pt.text)
.attr({
'class': 'slicetext',
transform: '',
'text-anchor': 'middle'
})
.call(Drawing.font, textPosition === 'outside' ?
determineOutsideTextFont(trace, pt, gd._fullLayout.font) :
determineInsideTextFont(trace, pt, gd._fullLayout.font))
.call(Drawing.font, font)
.call(svgTextUtils.convertToTspans, gd);

// position the text relative to the slice
Expand All @@ -164,36 +169,39 @@ function plot(gd, cdModule) {
} else {
transform = transformInsideText(textBB, pt, cd0);
if(textPosition === 'auto' && transform.scale < 1) {
sliceText.call(Drawing.font, trace.outsidetextfont);
if(trace.outsidetextfont.family !== trace.insidetextfont.family ||
trace.outsidetextfont.size !== trace.insidetextfont.size) {
var newFont = Lib.extendFlat({}, trace.outsidetextfont, {});
newFont.size = Math.max(newFont.size, fullLayout.uniformtext.minsize || 0);

sliceText.call(Drawing.font, newFont);
if(newFont.family !== font.family || newFont.size !== font.size) {
etpinard marked this conversation as resolved.
Show resolved Hide resolved
// recompute bounding box
textBB = Drawing.bBox(sliceText.node());
}
transform = transformOutsideText(textBB, pt);
}
}

var translateX = cx + pt.pxmid[0] * transform.rCenter + (transform.x || 0);
var translateY = cy + pt.pxmid[1] * transform.rCenter + (transform.y || 0);
var pxtxt = pt.pxtxt || pt.pxmid;
transform.targetX = cx + pxtxt[0] * transform.rCenter + (transform.x || 0);
transform.targetY = cy + pxtxt[1] * transform.rCenter + (transform.y || 0);
computeTransform(transform, textBB);

// save some stuff to use later ensure no labels overlap
if(transform.outside) {
pt.yLabelMin = translateY - textBB.height / 2;
pt.yLabelMid = translateY;
pt.yLabelMax = translateY + textBB.height / 2;
var targetY = transform.targetY;
pt.yLabelMin = targetY - textBB.height / 2;
pt.yLabelMid = targetY;
pt.yLabelMax = targetY + textBB.height / 2;
pt.labelExtraX = 0;
pt.labelExtraY = 0;
hasOutsideText = true;
}

sliceText.attr('transform',
'translate(' + translateX + ',' + translateY + ')' +
(transform.scale < 1 ? ('scale(' + transform.scale + ')') : '') +
(transform.rotate ? ('rotate(' + transform.rotate + ')') : '') +
'translate(' +
(-(textBB.left + textBB.right) / 2) + ',' +
(-(textBB.top + textBB.bottom) / 2) +
')');
transform.fontSize = font.size;
recordMinTextSize(trace.type, transform, fullLayout);
cd[i].transform = transform;
etpinard marked this conversation as resolved.
Show resolved Hide resolved

sliceText.attr('transform', Lib.getTextTransform(transform, true));
});
});

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

sliceText.attr('transform', 'translate(' + pt.labelExtraX + ',' + pt.labelExtraY + ')' +
sliceText.attr('transform'));
pt.transform.targetX += pt.labelExtraX;
pt.transform.targetY += pt.labelExtraY;

sliceText.attr('transform', Lib.getTextTransform(pt.transform));

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

function transformInsideText(textBB, pt, cd0) {
var textDiameter = Math.sqrt(textBB.width * textBB.width + textBB.height * textBB.height);
var textAspect = textBB.width / textBB.height;
var halfAngle = pt.halfangle;
var midAngle = pt.midangle;
var ring = pt.ring;
var rInscribed = pt.rInscribed;
var r = cd0.r || pt.rpx1;
var orientation = cd0.trace.insidetextorientation;
var allTransforms = [];

var isCircle = (ring === 1) && (Math.abs(pt.startangle - pt.stopangle) === Math.PI * 2);

if(isCircle || orientation === 'auto' || orientation === 'h') {
// max size text can be inserted inside without rotating it
// this inscribes the text rectangle in a circle, which is then inscribed
// in the slice, so it will be an underestimate, which some day we may want
// to improve so this case can get more use
var transform = {
scale: rInscribed * r * 2 / textDiameter,

// and the center position and rotation in this case
rCenter: 1 - rInscribed,
rotate: 0
};

// max size text can be inserted inside without rotating it
// this inscribes the text rectangle in a circle, which is then inscribed
// in the slice, so it will be an underestimate, which some day we may want
// to improve so this case can get more use
var transform = {
scale: rInscribed * r * 2 / textDiameter,
if(transform.scale >= 1) return transform;
etpinard marked this conversation as resolved.
Show resolved Hide resolved

// and the center position and rotation in this case
rCenter: 1 - rInscribed,
rotate: 0
};
allTransforms.push(transform);
}

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

// max size if text is rotated radially
var Qr = textAspect + 1 / (2 * Math.tan(halfAngle));
var maxHalfHeightRotRadial = r * Math.min(
1 / (Math.sqrt(Qr * Qr + 0.5) + Qr),
ring / (Math.sqrt(textAspect * textAspect + ring / 2) + textAspect)
var considerCrossing = function(angle, key) {
if(isCrossing(pt, angle)) {
var dStart = Math.abs(angle - pt.startangle);
var dStop = Math.abs(angle - pt.stopangle);

var closestEdge = dStart < dStop ? dStart : dStop;

var newT;
if(key === 'tan') {
newT = calcTanTransform(textBB, r, ring, closestEdge, 0);
} else { // case of 'rad'
newT = calcRadTransform(textBB, r, ring, closestEdge, Math.PI / 2);
}
newT._repos = getCoords(r, angle);
etpinard marked this conversation as resolved.
Show resolved Hide resolved

allTransforms.push(newT);
}
};

for(var i = 3; i >= -3; i--) { // to cover all cases with trace.rotation added
considerCrossing(Math.PI * (i + 0.0), 'tan');
etpinard marked this conversation as resolved.
Show resolved Hide resolved
considerCrossing(Math.PI * (i + 0.5), 'rad');
}
}

if(orientation === 'auto' || orientation === 'r') {
allTransforms.push(calcRadTransform(textBB, r, ring, halfAngle, midAngle));
}

if(orientation === 'auto' || orientation === 't') {
allTransforms.push(calcTanTransform(textBB, r, ring, halfAngle, midAngle));
}

var maxScaleTransform = allTransforms.sort(function(a, b) {
return b.scale - a.scale;
})[0];

if(maxScaleTransform._repos) {
pt.pxtxt = maxScaleTransform._repos;
}

return maxScaleTransform;
}

function isCrossing(pt, angle) {
var start = pt.startangle;
var stop = pt.stopangle;
return (
(start > angle && angle > stop) ||
(start < angle && angle < stop)
);
var radialTransform = {
scale: maxHalfHeightRotRadial * 2 / textBB.height,
rCenter: Math.cos(maxHalfHeightRotRadial / r) -
maxHalfHeightRotRadial * textAspect / r,
rotate: (180 / Math.PI * pt.midangle + 720) % 180 - 90
}

function calcRadTransform(textBB, r, ring, halfAngle, midAngle) {
// max size if text is rotated radially
var a = textBB.width / textBB.height;
var s = calcMaxHalfSize(a, halfAngle, r, ring);
return {
scale: s * 2 / textBB.height,
rCenter: calcRCenter(a, s / r),
rotate: calcRotate(midAngle)
};
}

function calcTanTransform(textBB, r, ring, halfAngle, midAngle) {
// max size if text is rotated tangentially
var aspectInv = 1 / textAspect;
var Qt = aspectInv + 1 / (2 * Math.tan(halfAngle));
var maxHalfWidthTangential = r * Math.min(
1 / (Math.sqrt(Qt * Qt + 0.5) + Qt),
ring / (Math.sqrt(aspectInv * aspectInv + ring / 2) + aspectInv)
);
var tangentialTransform = {
scale: maxHalfWidthTangential * 2 / textBB.width,
rCenter: Math.cos(maxHalfWidthTangential / r) -
maxHalfWidthTangential / textAspect / r,
rotate: (180 / Math.PI * pt.midangle + 810) % 180 - 90
var a = textBB.height / textBB.width;
var s = calcMaxHalfSize(a, halfAngle, r, ring);
return {
scale: s * 2 / textBB.width,
rCenter: calcRCenter(a, s / r),
rotate: calcRotate(midAngle + Math.PI / 2)
};
// if we need a rotated transform, pick the biggest one
// even if both are bigger than 1
var rotatedTransform = tangentialTransform.scale > radialTransform.scale ?
tangentialTransform : radialTransform;
}

function calcRCenter(a, b) {
return Math.cos(b) - a * b;
}

function calcRotate(t) {
return (180 / Math.PI * t + 720) % 180 - 90;
}

if(transform.scale < 1 && rotatedTransform.scale > transform.scale) return rotatedTransform;
return transform;
function calcMaxHalfSize(a, halfAngle, r, ring) {
var q = a + 1 / (2 * Math.tan(halfAngle));
return r * Math.min(
1 / (Math.sqrt(q * q + 0.5) + q),
ring / (Math.sqrt(a * a + ring / 2) + a)
);
}

function getInscribedRadiusFraction(pt, cd0) {
Expand Down Expand Up @@ -921,6 +998,7 @@ function groupScale(cdModule, scaleGroups) {

function setCoords(cd) {
var cd0 = cd[0];
var r = cd0.r;
var trace = cd0.trace;
var currentAngle = trace.rotation * Math.PI / 180;
var angleFactor = 2 * Math.PI / cd0.vTotal;
Expand All @@ -941,24 +1019,21 @@ function setCoords(cd) {
lastPt = 'px0';
}

function getCoords(angle) {
return [cd0.r * Math.sin(angle), -cd0.r * Math.cos(angle)];
}

currentCoords = getCoords(currentAngle);
currentCoords = getCoords(r, currentAngle);

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

cdi[firstPt] = currentCoords;

cdi.startangle = currentAngle;
currentAngle += angleFactor * cdi.v / 2;
cdi.pxmid = getCoords(currentAngle);
cdi.pxmid = getCoords(r, currentAngle);
cdi.midangle = currentAngle;

currentAngle += angleFactor * cdi.v / 2;
currentCoords = getCoords(currentAngle);
currentCoords = getCoords(r, currentAngle);
cdi.stopangle = currentAngle;

cdi[lastPt] = currentCoords;

Expand All @@ -970,6 +1045,10 @@ function setCoords(cd) {
}
}

function getCoords(r, angle) {
return [r * Math.sin(angle), -r * Math.cos(angle)];
}

function formatSliceLabel(gd, pt, cd0) {
var fullLayout = gd._fullLayout;
var trace = cd0.trace;
Expand Down Expand Up @@ -1024,6 +1103,24 @@ function formatSliceLabel(gd, pt, cd0) {
}
}
}

function computeTransform(
transform, // inout
textBB // in
) {
var rotate = transform.rotate;
var scale = transform.scale;
if(scale > 1) scale = 1;

var a = rotate * Math.PI / 180;
var cosA = Math.cos(a);
var sinA = Math.sin(a);
var midX = (textBB.left + textBB.right) / 2;
var midY = (textBB.top + textBB.bottom) / 2;
transform.textX = midX * cosA - midY * sinA;
transform.textY = midX * sinA + midY * cosA;
}

module.exports = {
plot: plot,
formatSliceLabel: formatSliceLabel,
Expand All @@ -1033,4 +1130,5 @@ module.exports = {
prerenderTitles: prerenderTitles,
layoutAreas: layoutAreas,
attachFxHandlers: attachFxHandlers,
computeTransform: computeTransform
};
Loading