Skip to content

Commit b9a1705

Browse files
committed
Merge pull request #417 from n-riesco/issue-384-legend-negative-y-no-strict-margins
Fix issue #384 (invisible legends when y is negative)
2 parents 6c777f0 + d5e9014 commit b9a1705

File tree

4 files changed

+189
-68
lines changed

4 files changed

+189
-68
lines changed

src/components/legend/draw.js

+109-60
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,18 @@ module.exports = function draw(gd) {
173173
});
174174

175175
// Position and size the legend
176-
repositionLegend(gd, traces);
176+
var lyMin = 0,
177+
lyMax = fullLayout.height;
178+
179+
computeLegendDimensions(gd, traces);
180+
181+
if(opts.height > lyMax) {
182+
// If the legend doesn't fit in the plot area,
183+
// do not expand the vertical margins.
184+
expandHorizontalMargin(gd);
185+
} else {
186+
expandMargin(gd);
187+
}
177188

178189
// Scroll section must be executed after repositionLegend.
179190
// It requires the legend width, height, x and y to position the scrollbox
@@ -185,27 +196,41 @@ module.exports = function draw(gd) {
185196
if(anchorUtils.isRightAnchor(opts)) {
186197
lx -= opts.width;
187198
}
188-
if(anchorUtils.isCenterAnchor(opts)) {
199+
else if(anchorUtils.isCenterAnchor(opts)) {
189200
lx -= opts.width / 2;
190201
}
191202

192203
if(anchorUtils.isBottomAnchor(opts)) {
193204
ly -= opts.height;
194205
}
195-
if(anchorUtils.isMiddleAnchor(opts)) {
206+
else if(anchorUtils.isMiddleAnchor(opts)) {
196207
ly -= opts.height / 2;
197208
}
198209

210+
// Make sure the legend top and bottom are visible
211+
// (legends with a scroll bar are not allowed to stretch beyond the extended
212+
// margins)
213+
var legendHeight = opts.height,
214+
legendHeightMax = gs.h;
215+
216+
if(legendHeight > legendHeightMax) {
217+
ly = gs.t;
218+
legendHeight = legendHeightMax;
219+
}
220+
else {
221+
if(ly > lyMax) ly = lyMax - legendHeight;
222+
if(ly < lyMin) ly = lyMin;
223+
legendHeight = Math.min(lyMax - ly, opts.height);
224+
}
225+
199226
// Deal with scrolling
200-
var plotHeight = fullLayout.height - fullLayout.margin.b,
201-
scrollheight = Math.min(plotHeight - ly, opts.height),
202-
scrollPosition = scrollBox.attr('data-scroll') ? scrollBox.attr('data-scroll') : 0;
227+
var scrollPosition = scrollBox.attr('data-scroll') || 0;
203228

204229
scrollBox.attr('transform', 'translate(0, ' + scrollPosition + ')');
205230

206231
bg.attr({
207232
width: opts.width - 2 * opts.borderwidth,
208-
height: scrollheight - 2 * opts.borderwidth,
233+
height: legendHeight - 2 * opts.borderwidth,
209234
x: opts.borderwidth,
210235
y: opts.borderwidth
211236
});
@@ -214,71 +239,81 @@ module.exports = function draw(gd) {
214239

215240
clipPath.select('rect').attr({
216241
width: opts.width,
217-
height: scrollheight,
242+
height: legendHeight,
218243
x: 0,
219244
y: 0
220245
});
221246

222247
legend.call(Drawing.setClipUrl, clipId);
223248

224249
// If scrollbar should be shown.
225-
if(opts.height - scrollheight > 0 && !gd._context.staticPlot) {
250+
if(opts.height - legendHeight > 0 && !gd._context.staticPlot) {
226251

252+
// increase the background and clip-path width
253+
// by the scrollbar width and margin
227254
bg.attr({
228-
width: opts.width - 2 * opts.borderwidth + constants.scrollBarWidth
255+
width: opts.width -
256+
2 * opts.borderwidth +
257+
constants.scrollBarWidth +
258+
constants.scrollBarMargin
229259
});
230260

231-
clipPath.attr({
232-
width: opts.width + constants.scrollBarWidth
261+
clipPath.select('rect').attr({
262+
width: opts.width +
263+
constants.scrollBarWidth +
264+
constants.scrollBarMargin
233265
});
234266

235267
if(gd.firstRender) {
236268
// Move scrollbar to starting position
237-
scrollBar.call(
238-
Drawing.setRect,
239-
opts.width - (constants.scrollBarWidth + constants.scrollBarMargin),
240-
constants.scrollBarMargin,
241-
constants.scrollBarWidth,
242-
constants.scrollBarHeight
243-
);
244-
scrollBox.attr('data-scroll',0);
269+
scrollHandler(constants.scrollBarMargin, 0);
245270
}
246271

247-
scrollHandler(0,scrollheight);
272+
var scrollBarYMax = legendHeight -
273+
constants.scrollBarHeight -
274+
2 * constants.scrollBarMargin,
275+
scrollBoxYMax = opts.height - legendHeight,
276+
scrollBarY = constants.scrollBarMargin,
277+
scrollBoxY = 0;
248278

249-
legend.on('wheel',null);
279+
scrollHandler(scrollBarY, scrollBoxY);
250280

281+
legend.on('wheel',null);
251282
legend.on('wheel', function() {
252-
var e = d3.event;
253-
e.preventDefault();
254-
scrollHandler(e.deltaY / 20, scrollheight);
283+
scrollBoxY = Lib.constrain(
284+
scrollBox.attr('data-scroll') -
285+
d3.event.deltaY / scrollBarYMax * scrollBoxYMax,
286+
-scrollBoxYMax, 0);
287+
scrollBarY = constants.scrollBarMargin -
288+
scrollBoxY / scrollBoxYMax * scrollBarYMax;
289+
scrollHandler(scrollBarY, scrollBoxY);
290+
d3.event.preventDefault();
255291
});
256292

257293
scrollBar.on('.drag',null);
258294
scrollBox.on('.drag',null);
259-
var drag = d3.behavior.drag()
260-
.on('drag', function() {
261-
scrollHandler(d3.event.dy, scrollheight);
262-
});
295+
var drag = d3.behavior.drag().on('drag', function() {
296+
scrollBarY = Lib.constrain(
297+
d3.event.y - constants.scrollBarHeight / 2,
298+
constants.scrollBarMargin,
299+
constants.scrollBarMargin + scrollBarYMax);
300+
scrollBoxY = - (scrollBarY - constants.scrollBarMargin) /
301+
scrollBarYMax * scrollBoxYMax;
302+
scrollHandler(scrollBarY, scrollBoxY);
303+
});
263304

264305
scrollBar.call(drag);
265306
scrollBox.call(drag);
266307

267308
}
268309

269310

270-
function scrollHandler(delta, scrollheight) {
271-
272-
var scrollBarTrack = scrollheight - constants.scrollBarHeight - 2 * constants.scrollBarMargin,
273-
translateY = scrollBox.attr('data-scroll'),
274-
scrollBoxY = Lib.constrain(translateY - delta, scrollheight-opts.height, 0),
275-
scrollBarY = -scrollBoxY / (opts.height - scrollheight) * scrollBarTrack + constants.scrollBarMargin;
276-
311+
function scrollHandler(scrollBarY, scrollBoxY) {
277312
scrollBox.attr('data-scroll', scrollBoxY);
278313
scrollBox.attr('transform', 'translate(0, ' + scrollBoxY + ')');
279314
scrollBar.call(
280315
Drawing.setRect,
281-
opts.width - (constants.scrollBarWidth + constants.scrollBarMargin),
316+
opts.width,
282317
scrollBarY,
283318
constants.scrollBarWidth,
284319
constants.scrollBarHeight
@@ -348,7 +383,10 @@ function drawTexts(context, gd, d, i, traces) {
348383

349384
function textLayout(s) {
350385
Plotly.util.convertToTspans(s, function() {
351-
if(gd.firstRender) repositionLegend(gd, traces);
386+
if(gd.firstRender) {
387+
computeLegendDimensions(gd, traces);
388+
expandMargin(gd);
389+
}
352390
});
353391
s.selectAll('tspan.line').attr({x: s.attr('x')});
354392
}
@@ -367,9 +405,8 @@ function drawTexts(context, gd, d, i, traces) {
367405
else text.call(textLayout);
368406
}
369407

370-
function repositionLegend(gd, traces) {
408+
function computeLegendDimensions(gd, traces) {
371409
var fullLayout = gd._fullLayout,
372-
gs = fullLayout._size,
373410
opts = fullLayout.legend,
374411
borderwidth = opts.borderwidth;
375412

@@ -421,7 +458,6 @@ function repositionLegend(gd, traces) {
421458
opts.width = Math.max(opts.width, tWidth || 0);
422459
});
423460

424-
425461
opts.width += 45 + borderwidth * 2;
426462
opts.height += 10 + borderwidth * 2;
427463

@@ -432,41 +468,31 @@ function repositionLegend(gd, traces) {
432468
traces.selectAll('.legendtoggle')
433469
.attr('width', (gd._context.editable ? 0 : opts.width) + 40);
434470

435-
// now position the legend. for both x,y the positions are recorded as
436-
// fractions of the plot area (left, bottom = 0,0). Outside the plot
437-
// area is allowed but position will be clipped to the page.
438-
// values <1/3 align the low side at that fraction, 1/3-2/3 align the
439-
// center at that fraction, >2/3 align the right at that fraction
471+
// make sure we're only getting full pixels
472+
opts.width = Math.ceil(opts.width);
473+
opts.height = Math.ceil(opts.height);
474+
}
440475

441-
var lx = gs.l + gs.w * opts.x,
442-
ly = gs.t + gs.h * (1-opts.y);
476+
function expandMargin(gd) {
477+
var fullLayout = gd._fullLayout,
478+
opts = fullLayout.legend;
443479

444480
var xanchor = 'left';
445481
if(anchorUtils.isRightAnchor(opts)) {
446-
lx -= opts.width;
447482
xanchor = 'right';
448483
}
449-
if(anchorUtils.isCenterAnchor(opts)) {
450-
lx -= opts.width / 2;
484+
else if(anchorUtils.isCenterAnchor(opts)) {
451485
xanchor = 'center';
452486
}
453487

454488
var yanchor = 'top';
455489
if(anchorUtils.isBottomAnchor(opts)) {
456-
ly -= opts.height;
457490
yanchor = 'bottom';
458491
}
459-
if(anchorUtils.isMiddleAnchor(opts)) {
460-
ly -= opts.height / 2;
492+
else if(anchorUtils.isMiddleAnchor(opts)) {
461493
yanchor = 'middle';
462494
}
463495

464-
// make sure we're only getting full pixels
465-
opts.width = Math.ceil(opts.width);
466-
opts.height = Math.ceil(opts.height);
467-
lx = Math.round(lx);
468-
ly = Math.round(ly);
469-
470496
// lastly check if the margin auto-expand has changed
471497
Plots.autoMargin(gd, 'legend', {
472498
x: opts.x,
@@ -477,3 +503,26 @@ function repositionLegend(gd, traces) {
477503
t: opts.height * ({bottom: 1, middle: 0.5}[yanchor] || 0)
478504
});
479505
}
506+
507+
function expandHorizontalMargin(gd) {
508+
var fullLayout = gd._fullLayout,
509+
opts = fullLayout.legend;
510+
511+
var xanchor = 'left';
512+
if(anchorUtils.isRightAnchor(opts)) {
513+
xanchor = 'right';
514+
}
515+
else if(anchorUtils.isCenterAnchor(opts)) {
516+
xanchor = 'center';
517+
}
518+
519+
// lastly check if the margin auto-expand has changed
520+
Plots.autoMargin(gd, 'legend', {
521+
x: opts.x,
522+
y: 0.5,
523+
l: opts.width * ({right: 1, center: 0.5}[xanchor] || 0),
524+
r: opts.width * ({left: 1, center: 0.5}[xanchor] || 0),
525+
b: 0,
526+
t: 0
527+
});
528+
}
21.3 KB
Loading
+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
{
2+
"data":[
3+
{
4+
"x":[
5+
0,
6+
1,
7+
2,
8+
3,
9+
4,
10+
5,
11+
6,
12+
7,
13+
8
14+
],
15+
"y":[
16+
0,
17+
3,
18+
6,
19+
4,
20+
5,
21+
2,
22+
3,
23+
5,
24+
4
25+
],
26+
"type":"scatter"
27+
},
28+
{
29+
"x":[
30+
0,
31+
1,
32+
2,
33+
3,
34+
4,
35+
5,
36+
6,
37+
7,
38+
8
39+
],
40+
"y":[
41+
0,
42+
4,
43+
7,
44+
8,
45+
3,
46+
6,
47+
3,
48+
3,
49+
4
50+
],
51+
"type":"scatter"
52+
}
53+
],
54+
"layout":{
55+
"showlegend":true,
56+
"legend":{
57+
"x":1,
58+
"y":-1,
59+
"xanchor":"left"
60+
}
61+
}
62+
}

test/jasmine/tests/legend_scroll_test.js

+18-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
var Plotly = require('@lib/index');
22
var Lib = require('@src/lib');
3+
var constants = require('@src/components/legend/constants');
34

45
var createGraph = require('../assets/create_graph_div');
56
var destroyGraph = require('../assets/destroy_graph_div');
@@ -55,14 +56,23 @@ describe('The legend', function() {
5556
});
5657

5758
it('should scroll when there\'s a wheel event', function() {
58-
var scrollBox = legend.getElementsByClassName('scrollbox')[0];
59-
60-
legend.dispatchEvent(scrollTo(100));
61-
62-
// Compare against -5 because of a scroll factor of 20
63-
// ( 100 / 20 === 5 )
64-
expect(scrollBox.getAttribute('transform')).toBe('translate(0, -5)');
65-
expect(scrollBox.getAttribute('data-scroll')).toBe('-5');
59+
var scrollBox = legend.getElementsByClassName('scrollbox')[0],
60+
legendHeight = getBBox(legend).height,
61+
scrollBoxYMax = gd._fullLayout.legend.height - legendHeight,
62+
scrollBarYMax = legendHeight -
63+
constants.scrollBarHeight -
64+
2 * constants.scrollBarMargin,
65+
initialDataScroll = scrollBox.getAttribute('data-scroll'),
66+
wheelDeltaY = 100,
67+
finalDataScroll = '' + Lib.constrain(initialDataScroll -
68+
wheelDeltaY / scrollBarYMax * scrollBoxYMax,
69+
-scrollBoxYMax, 0);
70+
71+
legend.dispatchEvent(scrollTo(wheelDeltaY));
72+
73+
expect(scrollBox.getAttribute('data-scroll')).toBe(finalDataScroll);
74+
expect(scrollBox.getAttribute('transform')).toBe(
75+
'translate(0, ' + finalDataScroll + ')');
6676
});
6777

6878
it('should constrain scrolling to the contents', function() {

0 commit comments

Comments
 (0)