Skip to content

Commit d8d7d59

Browse files
committed
Move leaders with their labels when a selection changes the marker size
1 parent 63ca067 commit d8d7d59

3 files changed

Lines changed: 33 additions & 18 deletions

File tree

‎src/components/drawing/index.js‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1382,13 +1382,20 @@ drawing.textPointPosition = function (s, d, trace, markerRadius, dontTouchParent
13821382
var textPosition = getTextPosition(d, trace);
13831383

13841384
var offset;
1385+
let leaderSeg = d._tpAutoLeader;
13851386
// Skip the box for an *auto* label that the placement did not reach yet,
13861387
// because the placement measures every label in one pass right after this draw
13871388
if (isAuto && textPosition && d._tpAuto !== undefined) {
13881389
var symbol = d.mx || (trace.marker || {}).symbol;
13891390
var bb = drawing.textPointBBox(s);
13901391
offset = drawing.textPointBoxOffset(textPosition, markerRadius, symbol, bb, d._tpAutoGap);
13911392

1393+
// The placement set the leader for `d._tpAutoRadius`, so move each end by the change that another radius makes
1394+
if (leaderSeg && offset.leader && (markerRadius || 0) !== d._tpAutoRadius) {
1395+
const base = drawing.textPointBoxOffset(textPosition, d._tpAutoRadius, symbol, bb, d._tpAutoGap);
1396+
leaderSeg = leaderSeg.map((v, i) => v + offset.leader[i] - base.leader[i]);
1397+
}
1398+
13921399
// a MathJax render is aligned to the anchor it had when it rendered
13931400
group.select('svg.text-math').attr('x', +s.attr('x') - (bb.width * (1 - TEXTOFFSETSIGN[offset.anchor])) / 2);
13941401
} else {
@@ -1414,9 +1421,9 @@ drawing.textPointPosition = function (s, d, trace, markerRadius, dontTouchParent
14141421
// Skip the join when no leader is due and none is left over, because only *auto* draws leaders
14151422
const first = group.node().firstChild;
14161423
const hasLeader = first && first.nodeName === 'path' && first.classList.contains('textleader');
1417-
if (!d._tpAutoLeader && !hasLeader) return;
1424+
if (!leaderSeg && !hasLeader) return;
14181425

1419-
var leader = group.selectAll('path.textleader').data(d._tpAutoLeader ? [d._tpAutoLeader] : []);
1426+
const leader = group.selectAll('path.textleader').data(leaderSeg ? [leaderSeg] : []);
14201427
leader.exit().remove();
14211428
leader.enter().insert('path', ':first-child').classed('textleader', true).style('fill', 'none');
14221429
leader.each(function (seg) {

‎src/traces/scatter/auto_text_position.js‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const { textrank } = require('./attributes');
2020
* with the axes of its own subplot. The pass of the last group sees all traces,
2121
* and `_tpAuto` keeps the labels of the earlier passes in place where it can.
2222
*
23-
* Sets `_tpAuto`, `_tpAutoGap`, and `_tpAutoLeader` on every calcdata point
23+
* Sets `_tpAuto`, `_tpAutoGap`, `_tpAutoLeader`, and `_tpAutoRadius` on every calcdata point
2424
* with an *auto* position and repositions its `<text>` node. Does nothing
2525
* when no trace of the plot area uses *auto*, or in the plot of a range slider,
2626
* where labels are never placed.
@@ -126,6 +126,8 @@ module.exports = function autoTextPosition(gd, plotinfo, traceGroups) {
126126
d._tpAutoGap = out ? out.gap : 0;
127127
// in the plot frame, so the leader does not depend on the `<text>` node in mid-transition
128128
d._tpAutoLeader = out ? out.leader : null;
129+
// The marker radius of the leader, which a later draw can change with another marker size
130+
d._tpAutoRadius = label.radius;
129131
Drawing.textPointPosition(label.tx, d, label.trace, d.mrc);
130132
}
131133

‎test/jasmine/tests/scatter_test.js‎

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -784,6 +784,21 @@ describe('end-to-end scatter tests', function() {
784784
}).size();
785785
}
786786

787+
// Leaders whose label sits away from the end of the leader line
788+
function detachedLeaders() {
789+
let n = 0;
790+
d3SelectAll('.textpoint').each(function () {
791+
const leader = this.querySelector('path.textleader');
792+
if (!leader || this.style.display === 'none') return;
793+
const l = leader.getBoundingClientRect();
794+
const t = this.querySelector('text').getBoundingClientRect();
795+
const dx = Math.max(t.left - l.right, l.left - t.right, 0);
796+
const dy = Math.max(t.top - l.bottom, l.top - t.bottom, 0);
797+
if (Math.hypot(dx, dy) > 3) n++;
798+
});
799+
return n;
800+
}
801+
787802
function countOverlaps(boxes) {
788803
var n = 0;
789804
for(var i = 0; i < boxes.length; i++) {
@@ -1098,6 +1113,12 @@ describe('end-to-end scatter tests', function() {
10981113
.then(function() {
10991114
expect(textPointTransforms()).toEqual(transforms);
11001115
expect(countOverlaps(visibleLabelBoxes())).toBe(0);
1116+
return Plotly.restyle(gd, 'selected.marker.size', 30);
1117+
})
1118+
.then(function() {
1119+
// The selected points draw their labels around the larger selected marker
1120+
expect(textPointTransforms()).not.toEqual(transforms);
1121+
expect(detachedLeaders()).toBe(0, 'detached leaders after a selection');
11011122
return Plotly.relayout(gd, 'xaxis.range', [1, 3]);
11021123
})
11031124
.then(function() {
@@ -1304,21 +1325,6 @@ describe('end-to-end scatter tests', function() {
13041325
.then(done, done.fail);
13051326
});
13061327

1307-
// Leaders whose label sits away from the end of the leader line
1308-
function detachedLeaders() {
1309-
let n = 0;
1310-
d3SelectAll('.textpoint').each(function () {
1311-
const leader = this.querySelector('path.textleader');
1312-
if (!leader || this.style.display === 'none') return;
1313-
const l = leader.getBoundingClientRect();
1314-
const t = this.querySelector('text').getBoundingClientRect();
1315-
const dx = Math.max(t.left - l.right, l.left - t.right, 0);
1316-
const dy = Math.max(t.top - l.bottom, l.top - t.bottom, 0);
1317-
if (Math.hypot(dx, dy) > 3) n++;
1318-
});
1319-
return n;
1320-
}
1321-
13221328
it('should place the labels again after a pan and a reset of the axes', (done) => {
13231329
Plotly.newPlot(gd, [trace('A', 0, { zorder: 0 }), trace('B', 0.15, { zorder: 1 })], size())
13241330
.then(() => {

0 commit comments

Comments
 (0)