Skip to content

Commit 415a1fc

Browse files
authored
Merge pull request #7451 from plotly/7278-make-triangle-optional
feat: Make hover label triangle optional by adding `trace.hoverlabel.showarrow` and `layout.hoverlabel.showarrow` attributes
2 parents 7d40224 + e382d2f commit 415a1fc

File tree

9 files changed

+462
-58
lines changed

9 files changed

+462
-58
lines changed

draftlogs/7451_add.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Add `layout.hoverlabel.showarrow` (and `trace.hoverlabel.showarrow`) attribute to allow hiding the triangular caret that appears on the hover label box [[#7451](https://github.com/plotly/plotly.js/pull/7451)]

src/components/fx/attributes.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ module.exports = {
2121
}),
2222
align: extendFlat({}, hoverLabelAttrs.align, {arrayOk: true}),
2323
namelength: extendFlat({}, hoverLabelAttrs.namelength, {arrayOk: true}),
24+
showarrow: extendFlat({}, hoverLabelAttrs.showarrow),
2425
editType: 'none'
2526
}
2627
};

src/components/fx/calc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ module.exports = function calc(gd) {
4040
fillFn(trace.hoverlabel.font.variant, cd, 'htv');
4141
fillFn(trace.hoverlabel.namelength, cd, 'hnl');
4242
fillFn(trace.hoverlabel.align, cd, 'hta');
43+
fillFn(trace.hoverlabel.showarrow, cd, 'htsa');
4344
}
4445
};
4546

src/components/fx/hover.js

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1938,20 +1938,30 @@ function alignHoverText(hoverLabels, rotateLabels, scaleX, scaleY) {
19381938
var offsetY = offsets.y;
19391939

19401940
var isMiddle = anchor === 'middle';
1941+
// Get 'showarrow' attribute value from trace hoverlabel settings;
1942+
// if trace has no hoverlabel settings, we should show the arrow by default
1943+
var showArrow = 'hoverlabel' in d.trace ? d.trace.hoverlabel.showarrow : true;
19411944

1942-
g.select('path')
1943-
.attr('d', isMiddle ?
1945+
var pathStr;
1946+
if(isMiddle) {
19441947
// middle aligned: rect centered on data
1945-
('M-' + pX(d.bx / 2 + d.tx2width / 2) + ',' + pY(offsetY - d.by / 2) +
1946-
'h' + pX(d.bx) + 'v' + pY(d.by) + 'h-' + pX(d.bx) + 'Z') :
1948+
pathStr = 'M-' + pX(d.bx / 2 + d.tx2width / 2) + ',' + pY(offsetY - d.by / 2) +
1949+
'h' + pX(d.bx) + 'v' + pY(d.by) + 'h-' + pX(d.bx) + 'Z';
1950+
} else if(showArrow) {
19471951
// left or right aligned: side rect with arrow to data
1948-
('M0,0L' + pX(horzSign * HOVERARROWSIZE + offsetX) + ',' + pY(HOVERARROWSIZE + offsetY) +
1949-
'v' + pY(d.by / 2 - HOVERARROWSIZE) +
1950-
'h' + pX(horzSign * d.bx) +
1951-
'v-' + pY(d.by) +
1952-
'H' + pX(horzSign * HOVERARROWSIZE + offsetX) +
1953-
'V' + pY(offsetY - HOVERARROWSIZE) +
1954-
'Z'));
1952+
pathStr = 'M0,0L' + pX(horzSign * HOVERARROWSIZE + offsetX) + ',' + pY(HOVERARROWSIZE + offsetY) +
1953+
'v' + pY(d.by / 2 - HOVERARROWSIZE) +
1954+
'h' + pX(horzSign * d.bx) +
1955+
'v-' + pY(d.by) +
1956+
'H' + pX(horzSign * HOVERARROWSIZE + offsetX) +
1957+
'V' + pY(offsetY - HOVERARROWSIZE) +
1958+
'Z';
1959+
} else {
1960+
// left or right aligned: side rect without arrow
1961+
pathStr = 'M' + pX(horzSign * HOVERARROWSIZE + offsetX) + ',' + pY(offsetY - d.by / 2) +
1962+
'h' + pX(horzSign * d.bx) + 'v' + pY(d.by) + 'h' + pX(-horzSign * d.bx) + 'Z';
1963+
}
1964+
g.select('path').attr('d', pathStr);
19551965

19561966
var posX = offsetX + shiftX.textShiftX;
19571967
var posY = offsetY + d.ty0 - d.by / 2 + HOVERTEXTPAD;

src/components/fx/hoverlabel_defaults.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ module.exports = function handleHoverLabelDefaults(contIn, contOut, coerce, opts
3636
coerce('hoverlabel.bgcolor', opts.bgcolor);
3737
coerce('hoverlabel.bordercolor', opts.bordercolor);
3838
coerce('hoverlabel.namelength', opts.namelength);
39+
coerce('hoverlabel.showarrow', opts.showarrow);
3940
Lib.coerceFont(coerce, 'hoverlabel.font', opts.font);
4041
coerce('hoverlabel.align', opts.align);
4142
};

src/components/fx/layout_attributes.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,15 @@ module.exports = {
165165
'`namelength - 3` characters and add an ellipsis.'
166166
].join(' ')
167167
},
168+
showarrow: {
169+
valType: 'boolean',
170+
dflt: true,
171+
editType: 'none',
172+
description: [
173+
'Sets whether or not to show the hover label arrow/triangle',
174+
'pointing to the data point.'
175+
].join(' ')
176+
},
168177

169178
editType: 'none'
170179
},

test/jasmine/tests/fx_test.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,8 @@ describe('Fx defaults', function() {
179179
shadow: 'auto',
180180
},
181181
align: 'auto',
182-
namelength: 15
182+
namelength: 15,
183+
showarrow: true,
183184
});
184185

185186
expect(out.data[1].hoverlabel).toEqual({
@@ -197,7 +198,8 @@ describe('Fx defaults', function() {
197198
shadow: 'auto',
198199
},
199200
align: 'auto',
200-
namelength: 15
201+
namelength: 15,
202+
showarrow: true,
201203
});
202204

203205
expect(out.layout.annotations[0].hoverlabel).toEqual({

test/jasmine/tests/hover_label_test.js

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7105,3 +7105,100 @@ describe('hover on traces with (x|y)hoverformat', function() {
71057105
.then(done, done.fail);
71067106
});
71077107
});
7108+
7109+
describe('hoverlabel.showarrow', function() {
7110+
'use strict';
7111+
7112+
var gd;
7113+
7114+
beforeEach(function() {
7115+
gd = createGraphDiv();
7116+
});
7117+
7118+
afterEach(destroyGraphDiv);
7119+
7120+
function _hover(x, y) {
7121+
mouseEvent('mousemove', x, y);
7122+
Lib.clearThrottle();
7123+
}
7124+
7125+
function getHoverPath() {
7126+
var hoverLabels = d3SelectAll('g.hovertext');
7127+
if (hoverLabels.size() === 0) return null;
7128+
return hoverLabels.select('path').attr('d');
7129+
}
7130+
7131+
it('should show hover arrow by default', function(done) {
7132+
Plotly.newPlot(gd, [{
7133+
x: [1, 2, 3],
7134+
y: [1, 2, 1],
7135+
type: 'scatter',
7136+
mode: 'markers'
7137+
}], {
7138+
width: 400,
7139+
height: 400,
7140+
margin: {l: 50, t: 50, r: 50, b: 50}
7141+
})
7142+
.then(function() {
7143+
_hover(200, 70); // Hover over middle point
7144+
})
7145+
.then(delay(HOVERMINTIME * 1.1))
7146+
.then(function() {
7147+
var pathD = getHoverPath();
7148+
expect(pathD).not.toBeNull('hover path should exist');
7149+
// Arrow paths contain 'L' commands starting from 0,0
7150+
expect(pathD).toMatch(/^M0,0L/, 'path should contain arrow (L command from 0,0)');
7151+
})
7152+
.then(done, done.fail);
7153+
});
7154+
7155+
it('should hide hover arrow when showarrow is false', function(done) {
7156+
Plotly.newPlot(gd, [{
7157+
x: [1, 2, 3],
7158+
y: [1, 2, 1],
7159+
type: 'scatter',
7160+
mode: 'markers'
7161+
}], {
7162+
width: 400,
7163+
height: 400,
7164+
margin: {l: 50, t: 50, r: 50, b: 50},
7165+
hoverlabel: { showarrow: false }
7166+
})
7167+
.then(function() {
7168+
_hover(200, 70); // Hover over middle point
7169+
})
7170+
.then(delay(HOVERMINTIME * 1.1))
7171+
.then(function() {
7172+
var pathD = getHoverPath();
7173+
expect(pathD).not.toBeNull('hover path should exist');
7174+
// No-arrow paths should be simple rectangles (no 'L' commands starting at 0,0))
7175+
expect(pathD).not.toMatch(/^M0,0L/, 'path should not start at 0,0');
7176+
expect(pathD).toMatch(/^M[\d.-]+,[\d.-]+h/, 'path should start with some numeric point and move horizontally');
7177+
})
7178+
.then(done, done.fail);
7179+
});
7180+
7181+
it('should work at trace level', function(done) {
7182+
Plotly.newPlot(gd, [{
7183+
x: [1, 2, 3],
7184+
y: [1, 2, 1],
7185+
type: 'scatter',
7186+
mode: 'markers',
7187+
hoverlabel: { showarrow: false }
7188+
}], {
7189+
width: 400,
7190+
height: 400,
7191+
margin: {l: 50, t: 50, r: 50, b: 50}
7192+
})
7193+
.then(function() {
7194+
_hover(200, 70); // Hover over middle point
7195+
})
7196+
.then(delay(HOVERMINTIME * 1.1))
7197+
.then(function() {
7198+
var pathD = getHoverPath();
7199+
expect(pathD).not.toBeNull('hover path should exist');
7200+
expect(pathD).not.toMatch(/^M0,0L/, 'trace-level showarrow:false should hide arrow');
7201+
})
7202+
.then(done, done.fail);
7203+
});
7204+
});

0 commit comments

Comments
 (0)