diff --git a/src/components/legend/attributes.js b/src/components/legend/attributes.js
index a42e324f010..72d03f1b817 100644
--- a/src/components/legend/attributes.js
+++ b/src/components/legend/attributes.js
@@ -120,5 +120,15 @@ module.exports = {
'or *bottom* of the legend.'
].join(' ')
},
- editType: 'legend'
+ editType: 'legend',
+ valign: {
+ valType: 'enumerated',
+ values: ['top', 'middle', 'bottom'],
+ dflt: 'middle',
+ role: 'style',
+ editType: 'legend',
+ description: [
+ 'Sets the vertical alignment of the symbols with respect to their associated text.',
+ ].join(' ')
+ }
};
diff --git a/src/components/legend/defaults.js b/src/components/legend/defaults.js
index c82d175951f..6e8132212c2 100644
--- a/src/components/legend/defaults.js
+++ b/src/components/legend/defaults.js
@@ -103,5 +103,6 @@ module.exports = function legendDefaults(layoutIn, layoutOut, fullData) {
coerce('xanchor', defaultXAnchor);
coerce('y', defaultY);
coerce('yanchor', defaultYAnchor);
+ coerce('valign');
Lib.noneOrAll(containerIn, containerOut, ['x', 'y']);
};
diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js
index 30d3c66aa7e..4e82f243555 100644
--- a/src/components/legend/draw.js
+++ b/src/components/legend/draw.js
@@ -536,6 +536,7 @@ function computeTextDimensions(g, gd) {
// to avoid getBoundingClientRect
var textY = lineHeight * (0.3 + (1 - textLines) / 2);
svgTextUtils.positionText(text, constants.textOffsetX, textY);
+ legendItem.lineHeight = lineHeight;
}
height = Math.max(height, 16) + 3;
diff --git a/src/components/legend/style.js b/src/components/legend/style.js
index bed8680ecbc..e959e061dcb 100644
--- a/src/components/legend/style.js
+++ b/src/components/legend/style.js
@@ -25,6 +25,19 @@ module.exports = function style(s, gd) {
var layers = Lib.ensureSingle(traceGroup, 'g', 'layers');
layers.style('opacity', d[0].trace.opacity);
+ // Marker vertical alignment
+ var valign = gd._fullLayout.legend.valign;
+ var lineHeight = d[0].lineHeight;
+ var height = d[0].height;
+
+ if(valign === 'middle' || !lineHeight || !height) {
+ layers.attr('transform', null); // this here is a fun d3 trick to unset DOM attributes
+ } else {
+ var factor = {top: 1, bottom: -1}[valign];
+ var markerOffsetY = factor * (0.5 * (lineHeight - height + 3));
+ layers.attr('transform', 'translate(0,' + markerOffsetY + ')');
+ }
+
var fill = layers
.selectAll('g.legendfill')
.data([d]);
diff --git a/test/image/baselines/legend_valign_middle.png b/test/image/baselines/legend_valign_middle.png
new file mode 100644
index 00000000000..9b55d972738
Binary files /dev/null and b/test/image/baselines/legend_valign_middle.png differ
diff --git a/test/image/baselines/legend_valign_top.png b/test/image/baselines/legend_valign_top.png
new file mode 100644
index 00000000000..83b94233512
Binary files /dev/null and b/test/image/baselines/legend_valign_top.png differ
diff --git a/test/image/mocks/legend_valign_middle.json b/test/image/mocks/legend_valign_middle.json
new file mode 100644
index 00000000000..9cc477a8b70
--- /dev/null
+++ b/test/image/mocks/legend_valign_middle.json
@@ -0,0 +1,25 @@
+{
+ "data": [{
+ "y": [1, 5, 3, 4, 5],
+ "name": "Super long name
Super long name
Super long name
Super long name",
+ "type": "scatter",
+ "showlegend": true
+ },
+ {
+ "y": [3, 2, 5, 1, 5],
+ "name": "Also super long name
Also super long name
Also super long name",
+ "type": "scatter",
+ "showlegend": true
+ }
+ ],
+ "layout": {
+ "width": 800,
+ "legend": {
+ "bgcolor": "rgba(0,255,255,1)",
+ "valign": "middle",
+ "font": {
+ "size": 20
+ }
+ }
+ }
+}
diff --git a/test/image/mocks/legend_valign_top.json b/test/image/mocks/legend_valign_top.json
new file mode 100644
index 00000000000..54bf55611d7
--- /dev/null
+++ b/test/image/mocks/legend_valign_top.json
@@ -0,0 +1,25 @@
+{
+ "data": [{
+ "y": [1, 5, 3, 4, 5],
+ "name": "Super long name
Super long name
Super long name
Super long name",
+ "type": "scatter",
+ "showlegend": true
+ },
+ {
+ "y": [3, 2, 5, 1, 5],
+ "name": "Also super long name
Also super long name
Also super long name",
+ "type": "scatter",
+ "showlegend": true
+ }
+ ],
+ "layout": {
+ "width": 800,
+ "legend": {
+ "bgcolor": "rgba(0,255,255,1)",
+ "valign": "top",
+ "font": {
+ "size": 20
+ }
+ }
+ }
+}
diff --git a/test/jasmine/tests/legend_test.js b/test/jasmine/tests/legend_test.js
index 930fa8cd236..62aacfa360c 100644
--- a/test/jasmine/tests/legend_test.js
+++ b/test/jasmine/tests/legend_test.js
@@ -16,6 +16,8 @@ var createGraphDiv = require('../assets/create_graph_div');
var destroyGraphDiv = require('../assets/destroy_graph_div');
var assertPlotSize = require('../assets/custom_assertions').assertPlotSize;
+var Drawing = require('@src/components/drawing');
+
describe('legend defaults', function() {
'use strict';
@@ -665,6 +667,43 @@ describe('legend relayout update', function() {
.catch(failTest)
.then(done);
});
+
+ describe('should update legend valign', function() {
+ var mock = require('@mocks/legend_valign_top.json');
+ var gd;
+
+ beforeEach(function() {
+ gd = createGraphDiv();
+ });
+ afterEach(destroyGraphDiv);
+
+ function markerOffsetY() {
+ var translate = Drawing.getTranslate(d3.select('.legend .traces .layers'));
+ return translate.y;
+ }
+
+ it('it should translate markers', function(done) {
+ var mockCopy = Lib.extendDeep({}, mock);
+
+ var top, middle, bottom;
+ Plotly.plot(gd, mockCopy.data, mockCopy.layout)
+ .then(function() {
+ top = markerOffsetY();
+ return Plotly.relayout(gd, 'legend.valign', 'middle');
+ })
+ .then(function() {
+ middle = markerOffsetY();
+ expect(middle).toBeGreaterThan(top);
+ return Plotly.relayout(gd, 'legend.valign', 'bottom');
+ })
+ .then(function() {
+ bottom = markerOffsetY();
+ expect(bottom).toBeGreaterThan(middle);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+ });
});
describe('legend orientation change:', function() {