Skip to content

Commit c60424b

Browse files
committed
first cut at zoom/pan/scroll ax.matches behavior
1 parent 5ddbf2a commit c60424b

File tree

1 file changed

+103
-39
lines changed

1 file changed

+103
-39
lines changed

src/plots/cartesian/dragbox.js

+103-39
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,12 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
7676
// which are the x/y {ax._id: ax} hash objects and their values
7777
// for linked axis relative to this subplot
7878
var links;
79+
// similar to `links` but for matching axes
80+
var matches;
7981
// set to ew/ns val when active, set to '' when inactive
8082
var xActive, yActive;
8183
// are all axes in this subplot are fixed?
8284
var allFixedRanges;
83-
// is subplot constrained?
84-
var isSubplotConstrained;
8585
// do we need to edit x/y ranges?
8686
var editX, editY;
8787
// graph-wide optimization flags
@@ -119,10 +119,10 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
119119
yActive = isDirectionActive(yaxes, ns);
120120
allFixedRanges = !yActive && !xActive;
121121

122-
links = calcLinks(gd, xaHash, yaHash);
123-
isSubplotConstrained = links.isSubplotConstrained;
124-
editX = ew || isSubplotConstrained;
125-
editY = ns || isSubplotConstrained;
122+
links = calcLinks(gd, gd._fullLayout._axisConstraintGroups, xaHash, yaHash);
123+
matches = calcLinks(gd, gd._fullLayout._axisMatchGroups, xaHash, yaHash);
124+
editX = ew || links.isSubplotConstrained || matches.isSubplotConstrained;
125+
editY = ns || links.isSubplotConstrained || matches.isSubplotConstrained;
126126

127127
var fullLayout = gd._fullLayout;
128128
hasScatterGl = fullLayout._has('scattergl');
@@ -337,22 +337,36 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
337337
corners.attr('d', 'M0,0Z');
338338
}
339339

340-
if(isSubplotConstrained) {
340+
if(links.isSubplotConstrained) {
341341
if(dx > MINZOOM || dy > MINZOOM) {
342342
zoomMode = 'xy';
343343
if(dx / pw > dy / ph) {
344344
dy = dx * ph / pw;
345345
if(y0 > y1) box.t = y0 - dy;
346346
else box.b = y0 + dy;
347-
}
348-
else {
347+
} else {
349348
dx = dy * pw / ph;
350349
if(x0 > x1) box.l = x0 - dx;
351350
else box.r = x0 + dx;
352351
}
353352
corners.attr('d', xyCorners(box));
353+
} else {
354+
noZoom();
354355
}
355-
else {
356+
}
357+
else if(matches.isSubplotConstrained) {
358+
if(dx > MINZOOM || dy > MINZOOM) {
359+
zoomMode = 'xy';
360+
361+
var r0 = Math.min(box.l / pw, (ph - box.b) / ph);
362+
var r1 = Math.max(box.r / pw, (ph - box.t) / ph);
363+
364+
box.l = r0 * pw;
365+
box.r = r1 * pw;
366+
box.b = (1 - r0) * ph;
367+
box.t = (1 - r1) * ph;
368+
corners.attr('d', xyCorners(box));
369+
} else {
356370
noZoom();
357371
}
358372
}
@@ -399,10 +413,10 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
399413

400414
// TODO: edit linked axes in zoomAxRanges and in dragTail
401415
if(zoomMode === 'xy' || zoomMode === 'x') {
402-
zoomAxRanges(xaxes, box.l / pw, box.r / pw, updates, links.xaxes);
416+
zoomAxRanges(xaxes, box.l / pw, box.r / pw, updates, links.xaxes, matches.xaxes);
403417
}
404418
if(zoomMode === 'xy' || zoomMode === 'y') {
405-
zoomAxRanges(yaxes, (ph - box.b) / ph, (ph - box.t) / ph, updates, links.yaxes);
419+
zoomAxRanges(yaxes, (ph - box.b) / ph, (ph - box.t) / ph, updates, links.yaxes, matches.yaxes);
406420
}
407421

408422
removeZoombox(gd);
@@ -468,6 +482,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
468482
for(i = 0; i < xaxes.length; i++) {
469483
zoomWheelOneAxis(xaxes[i], xfrac, zoom);
470484
}
485+
updateMatchedAxes(matches.isSubplotConstrained ? yaxes : matches.xaxes, xaxes[0].range);
471486

472487
scrollViewBox[2] *= zoom;
473488
scrollViewBox[0] += scrollViewBox[2] * xfrac * (1 / zoom - 1);
@@ -478,6 +493,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
478493
for(i = 0; i < yaxes.length; i++) {
479494
zoomWheelOneAxis(yaxes[i], yfrac, zoom);
480495
}
496+
updateMatchedAxes(matches.isSubplotConstrained ? xaxes : matches.yaxes, yaxes[0].range);
481497

482498
scrollViewBox[3] *= zoom;
483499
scrollViewBox[1] += scrollViewBox[3] * (1 - yfrac) * (1 / zoom - 1);
@@ -513,9 +529,19 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
513529
// prevent axis drawing from monkeying with margins until we're done
514530
gd._fullLayout._replotting = true;
515531

532+
var matchedByXaxes;
533+
var matchesByYaxes;
534+
if(matches.isSubplotConstrained) {
535+
matchedByXaxes = yaxes;
536+
matchesByYaxes = xaxes;
537+
} else {
538+
matchedByXaxes = matches.xaxes;
539+
matchesByYaxes = matches.yaxes;
540+
}
541+
516542
if(xActive === 'ew' || yActive === 'ns') {
517-
if(xActive) dragAxList(xaxes, dx);
518-
if(yActive) dragAxList(yaxes, dy);
543+
if(xActive) dragAxList(xaxes, matchedByXaxes, dx);
544+
if(yActive) dragAxList(yaxes, matchesByYaxes, dy);
519545
updateSubplots([xActive ? -dx : 0, yActive ? -dy : 0, pw, ph]);
520546
ticksAndAnnotations();
521547
return;
@@ -546,7 +572,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
546572
(movedAx._rl[end] - movedAx._rl[otherEnd]);
547573
}
548574

549-
if(isSubplotConstrained && xActive && yActive) {
575+
if(links.isSubplotConstrained && xActive && yActive) {
550576
// dragging a corner of a constrained subplot:
551577
// respect the fixed corner, but harmonize dx and dy
552578
var dxySign = ((xActive === 'w') === (yActive === 'n')) ? 1 : -1;
@@ -566,7 +592,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
566592
var x0 = (xActive === 'w') ? dx : 0;
567593
var y0 = (yActive === 'n') ? dy : 0;
568594

569-
if(isSubplotConstrained) {
595+
if(links.isSubplotConstrained) {
570596
var i;
571597
if(!xActive && yActive.length === 1) {
572598
// dragging one end of the y axis of a constrained subplot
@@ -588,6 +614,8 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
588614
}
589615
}
590616

617+
updateMatchedAxes(matchedByXaxes, xaxes[0].range);
618+
updateMatchedAxes(matchesByYaxes, yaxes[0].range);
591619
updateSubplots([x0, y0, pw - dx, ph - dy]);
592620
ticksAndAnnotations();
593621
}
@@ -607,10 +635,12 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
607635
if(editX) {
608636
pushActiveAxIds(xaxes);
609637
pushActiveAxIds(links.xaxes);
638+
pushActiveAxIds(matches.xaxes);
610639
}
611640
if(editY) {
612641
pushActiveAxIds(yaxes);
613642
pushActiveAxIds(links.yaxes);
643+
pushActiveAxIds(matches.yaxes);
614644
}
615645

616646
updates = {};
@@ -629,9 +659,14 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
629659
if(gd._transitioningWithDuration) return;
630660

631661
var doubleClickConfig = gd._context.doubleClick;
632-
var axList = (xActive ? xaxes : []).concat(yActive ? yaxes : []);
633-
var attrs = {};
634662

663+
var axList = [];
664+
if(xActive) axList = axList.concat(xaxes);
665+
if(yActive) axList = axList.concat(yaxes);
666+
if(matches.xaxes) axList = axList.concat(matches.xaxes);
667+
if(matches.yaxes) axList = axList.concat(matches.yaxes);
668+
669+
var attrs = {};
635670
var ax, i, rangeInitial;
636671

637672
// For reset+autosize mode:
@@ -668,10 +703,10 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
668703
else if(doubleClickConfig === 'reset') {
669704
// when we're resetting, reset all linked axes too, so we get back
670705
// to the fully-auto-with-constraints situation
671-
if(xActive || isSubplotConstrained) axList = axList.concat(links.xaxes);
672-
if(yActive && !isSubplotConstrained) axList = axList.concat(links.yaxes);
706+
if(xActive || links.isSubplotConstrained) axList = axList.concat(links.xaxes);
707+
if(yActive && !links.isSubplotConstrained) axList = axList.concat(links.yaxes);
673708

674-
if(isSubplotConstrained) {
709+
if(links.isSubplotConstrained) {
675710
if(!xActive) axList = axList.concat(xaxes);
676711
else if(!yActive) axList = axList.concat(yaxes);
677712
}
@@ -713,10 +748,6 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
713748
], gd);
714749
}
715750

716-
// x/y scaleFactor stash,
717-
// minimizes number of per-point DOM updates in updateSubplots below
718-
var xScaleFactorOld, yScaleFactorOld;
719-
720751
// updateSubplots - find all plot viewboxes that should be
721752
// affected by this drag, and update them. look for all plots
722753
// sharing an affected axis (including the one being dragged),
@@ -768,6 +799,14 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
768799
if(editX2) {
769800
xScaleFactor2 = xScaleFactor;
770801
clipDx = ew ? viewBox[0] : getShift(xa, xScaleFactor2);
802+
} else if(matches.xaHash[xa._id]) {
803+
xScaleFactor2 = xScaleFactor;
804+
clipDx = viewBox[0] * xa._length / xa0._length;
805+
} else if(matches.yaHash[xa._id]) {
806+
xScaleFactor2 = yScaleFactor;
807+
clipDx = yActive === 'ns' ?
808+
-viewBox[1] * xa._length / ya0._length :
809+
getShift(xa, xScaleFactor2, {n: 'top', s: 'bottom'}[yActive]);
771810
} else {
772811
xScaleFactor2 = getLinkedScaleFactor(xa, xScaleFactor, yScaleFactor);
773812
clipDx = scaleAndGetShift(xa, xScaleFactor2);
@@ -776,6 +815,14 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
776815
if(editY2) {
777816
yScaleFactor2 = yScaleFactor;
778817
clipDy = ns ? viewBox[1] : getShift(ya, yScaleFactor2);
818+
} else if(matches.yaHash[ya._id]) {
819+
yScaleFactor2 = yScaleFactor;
820+
clipDy = viewBox[1] * ya._length / ya0._length;
821+
} else if(matches.xaHash[ya._id]) {
822+
yScaleFactor2 = xScaleFactor;
823+
clipDy = xActive === 'ew' ?
824+
-viewBox[0] * ya._length / xa0._length :
825+
getShift(ya, yScaleFactor2, {e: 'right', w: 'left'}[xActive]);
779826
} else {
780827
yScaleFactor2 = getLinkedScaleFactor(ya, xScaleFactor, yScaleFactor);
781828
clipDy = scaleAndGetShift(ya, yScaleFactor2);
@@ -809,16 +856,16 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
809856
// the scale of the trace group.
810857
// apply only when scale changes, as adjusting the scale of
811858
// all the points can be expansive.
812-
if(xScaleFactor2 !== xScaleFactorOld || yScaleFactor2 !== yScaleFactorOld) {
859+
if(xScaleFactor2 !== sp.xScaleFactor || yScaleFactor2 !== sp.yScaleFactor) {
813860
Drawing.setPointGroupScale(sp.zoomScalePts, xScaleFactor2, yScaleFactor2);
814861
Drawing.setTextPointsScale(sp.zoomScaleTxt, xScaleFactor2, yScaleFactor2);
815862
}
816863

817864
Drawing.hideOutsideRangePoints(sp.clipOnAxisFalseTraces, sp);
818865

819866
// update x/y scaleFactor stash
820-
xScaleFactorOld = xScaleFactor2;
821-
yScaleFactorOld = yScaleFactor2;
867+
sp.xScaleFactor = xScaleFactor2;
868+
sp.yScaleFactor = yScaleFactor2;
822869
}
823870
}
824871
}
@@ -832,7 +879,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
832879
if(editX && links.xaHash[ax._id]) {
833880
return xScaleFactor;
834881
}
835-
if(editY && (isSubplotConstrained ? links.xaHash : links.yaHash)[ax._id]) {
882+
if(editY && (links.isSubplotConstrained ? links.xaHash : links.yaHash)[ax._id]) {
836883
return yScaleFactor;
837884
}
838885
return 0;
@@ -847,8 +894,8 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) {
847894
return 0;
848895
}
849896

850-
function getShift(ax, scaleFactor) {
851-
return ax._length * (1 - scaleFactor) * FROM_TL[ax.constraintoward || 'middle'];
897+
function getShift(ax, scaleFactor, from) {
898+
return ax._length * (1 - scaleFactor) * FROM_TL[from || ax.constraintoward || 'middle'];
852899
}
853900

854901
return dragger;
@@ -900,7 +947,7 @@ function getEndText(ax, end) {
900947
}
901948
}
902949

903-
function zoomAxRanges(axList, r0Fraction, r1Fraction, updates, linkedAxes) {
950+
function zoomAxRanges(axList, r0Fraction, r1Fraction, updates, linkedAxes, matchedAxes) {
904951
var i,
905952
axi,
906953
axRangeLinear0,
@@ -922,14 +969,22 @@ function zoomAxRanges(axList, r0Fraction, r1Fraction, updates, linkedAxes) {
922969
}
923970

924971
// zoom linked axes about their centers
925-
if(linkedAxes && linkedAxes.length) {
972+
if(linkedAxes.length) {
926973
var linkedR0Fraction = (r0Fraction + (1 - r1Fraction)) / 2;
974+
zoomAxRanges(linkedAxes, linkedR0Fraction, 1 - linkedR0Fraction, updates, [], []);
975+
}
927976

928-
zoomAxRanges(linkedAxes, linkedR0Fraction, 1 - linkedR0Fraction, updates);
977+
// TODO is picking the first ax always ok in general?
978+
// What if we're matching an overlaying axis?
979+
var rng = axList[0].range;
980+
for(i = 0; i < matchedAxes.length; i++) {
981+
axi = matchedAxes[i];
982+
updates[axi._name + '.range[0]'] = rng[0];
983+
updates[axi._name + '.range[1]'] = rng[1];
929984
}
930985
}
931986

932-
function dragAxList(axList, pix) {
987+
function dragAxList(axList, matchedAxes, pix) {
933988
for(var i = 0; i < axList.length; i++) {
934989
var axi = axList[i];
935990
if(!axi.fixedrange) {
@@ -939,6 +994,16 @@ function dragAxList(axList, pix) {
939994
];
940995
}
941996
}
997+
998+
updateMatchedAxes(matchedAxes, axList[0].range);
999+
}
1000+
1001+
function updateMatchedAxes(matchedAxes, rng) {
1002+
// TODO is picking the first ax always ok in general?
1003+
// What if we're matching an overlaying axis?
1004+
for(var i = 0; i < matchedAxes.length; i++) {
1005+
matchedAxes[i].range = rng.slice();
1006+
}
9421007
}
9431008

9441009
// common transform for dragging one end of an axis
@@ -1052,15 +1117,14 @@ function xyCorners(box) {
10521117
'h' + clen + 'v3h-' + (clen + 3) + 'Z';
10531118
}
10541119

1055-
function calcLinks(gd, xaHash, yaHash) {
1056-
var constraintGroups = gd._fullLayout._axisConstraintGroups;
1120+
function calcLinks(gd, groups, xaHash, yaHash) {
10571121
var isSubplotConstrained = false;
10581122
var xLinks = {};
10591123
var yLinks = {};
10601124
var xID, yID, xLinkID, yLinkID;
10611125

1062-
for(var i = 0; i < constraintGroups.length; i++) {
1063-
var group = constraintGroups[i];
1126+
for(var i = 0; i < groups.length; i++) {
1127+
var group = groups[i];
10641128
// check if any of the x axes we're dragging is in this constraint group
10651129
for(xID in xaHash) {
10661130
if(group[xID]) {

0 commit comments

Comments
 (0)