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() {