diff --git a/src/components/legend/attributes.js b/src/components/legend/attributes.js index 8e2456ef157..07e4785fbe9 100644 --- a/src/components/legend/attributes.js +++ b/src/components/legend/attributes.js @@ -92,6 +92,14 @@ module.exports = { 'or remain *constant* independent of the symbol size on the graph.' ].join(' ') }, + itemwidth: { + valType: 'number', + min: 30, + dflt: 30, + role: 'style', + editType: 'legend', + description: 'Sets the width (in px) of the legend item symbols (the part other than the title.text).', + }, itemclick: { valType: 'enumerated', diff --git a/src/components/legend/constants.js b/src/components/legend/constants.js index 67a0468c3d2..8a213bf6413 100644 --- a/src/components/legend/constants.js +++ b/src/components/legend/constants.js @@ -17,8 +17,6 @@ module.exports = { // number of px between legend title and (left) side of legend (always in x direction and from inner border) titlePad: 2, - // number of px between legend symbol and legend text (always in x direction) - textGap: 40, // number of px between each legend item (x and/or y direction) itemGap: 5 }; diff --git a/src/components/legend/defaults.js b/src/components/legend/defaults.js index 9077a1e7890..06e2bce6d12 100644 --- a/src/components/legend/defaults.js +++ b/src/components/legend/defaults.js @@ -113,6 +113,7 @@ module.exports = function legendDefaults(layoutIn, layoutOut, fullData) { if(helpers.isGrouped(layoutOut.legend)) coerce('tracegroupgap'); coerce('itemsizing'); + coerce('itemwidth'); coerce('itemclick'); coerce('itemdoubleclick'); diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index b106ae7cb72..9240f7e44c1 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -412,7 +412,8 @@ function drawTexts(g, gd, opts) { .call(Drawing.font, opts.font) .text(isEditable ? ensureLength(name, maxNameLength) : name); - svgTextUtils.positionText(textEl, constants.textGap, 0); + var textGap = opts.itemwidth + constants.itemGap * 2; + svgTextUtils.positionText(textEl, textGap, 0); if(isEditable) { textEl.call(svgTextUtils.makeEditable, {gd: gd, text: name}) @@ -542,7 +543,8 @@ function computeTextDimensions(g, gd, opts) { // to avoid getBoundingClientRect var textY = lineHeight * ((textLines - 1) / 2 - 0.3); if(legendItem) { - svgTextUtils.positionText(textEl, constants.textGap, -textY); + var textGap = opts.itemwidth + constants.itemGap * 2; + svgTextUtils.positionText(textEl, textGap, -textY); } else { // case of title svgTextUtils.positionText(textEl, constants.titlePad + bw, lineHeight + bw); } @@ -595,8 +597,8 @@ function computeLegendDimensions(gd, groups, traces, opts) { var bw = opts.borderwidth; var bw2 = 2 * bw; - var textGap = constants.textGap; var itemGap = constants.itemGap; + var textGap = opts.itemwidth + itemGap * 2; var endPad = 2 * (bw + itemGap); var yanchor = getYanchor(opts); diff --git a/src/components/legend/style.js b/src/components/legend/style.js index b7b9bec7618..e34143cafe9 100644 --- a/src/components/legend/style.js +++ b/src/components/legend/style.js @@ -20,6 +20,8 @@ var subTypes = require('../../traces/scatter/subtypes'); var stylePie = require('../../traces/pie/style_one'); var pieCastOption = require('../../traces/pie/helpers').castOption; +var constants = require('./constants'); + var CST_MARKER_SIZE = 12; var CST_LINE_WIDTH = 5; var CST_MARKER_LINE_WIDTH = 2; @@ -30,6 +32,9 @@ module.exports = function style(s, gd, legend) { var fullLayout = gd._fullLayout; if(!legend) legend = fullLayout.legend; var constantItemSizing = legend.itemsizing === 'constant'; + var itemWidth = legend.itemwidth; + var centerPos = (itemWidth + constants.itemGap * 2) / 2; + var centerTransform = 'translate(' + centerPos + ',0)'; var boundLineWidth = function(mlw, cont, max, cst) { var v; @@ -161,7 +166,7 @@ module.exports = function style(s, gd, legend) { .data(showFill || showGradientFill ? [d] : []); fill.enter().append('path').classed('js-fill', true); fill.exit().remove(); - fill.attr('d', pathStart + 'h30v6h-30z') + fill.attr('d', pathStart + 'h' + itemWidth + 'v6h-' + itemWidth + 'z') .call(showFill ? Drawing.fillGroupStyle : fillGradient); if(showLine || showGradientLine) { @@ -181,7 +186,7 @@ module.exports = function style(s, gd, legend) { // though there *is* no vertical variation in this case. // so add an invisibly small angle to the line // This issue (and workaround) exist across (Mac) Chrome, FF, and Safari - line.attr('d', pathStart + (showGradientLine ? 'l30,0.0001' : 'h30')) + line.attr('d', pathStart + (showGradientLine ? 'l' + itemWidth + ',0.0001' : 'h' + itemWidth)) .call(showLine ? Drawing.lineGroupStyle : lineGradient); } @@ -271,7 +276,7 @@ module.exports = function style(s, gd, legend) { // make sure marker is on the bottom, in case it enters after text pts.enter().insert('path', ':first-child') .classed('scatterpts', true) - .attr('transform', 'translate(20,0)'); + .attr('transform', centerTransform); pts.exit().remove(); pts.call(Drawing.pointStyle, tMod, gd); @@ -283,7 +288,7 @@ module.exports = function style(s, gd, legend) { .data(showText ? dMod : []); txt.enter() .append('g').classed('pointtext', true) - .append('text').attr('transform', 'translate(20,0)'); + .append('text').attr('transform', centerTransform); txt.exit().remove(); txt.selectAll('text').call(Drawing.textPointStyle, tMod, gd); } @@ -311,7 +316,7 @@ module.exports = function style(s, gd, legend) { .selectAll('path.legendwaterfall') .data(ptsData); pts.enter().append('path').classed('legendwaterfall', true) - .attr('transform', 'translate(20,0)') + .attr('transform', centerTransform) .style('stroke-miterlimit', 1); pts.exit().remove(); @@ -351,7 +356,7 @@ module.exports = function style(s, gd, legend) { .data(isVisible ? [d] : []); barpath.enter().append('path').classed('legend' + desiredType, true) .attr('d', 'M6,6H-6V-6H6Z') - .attr('transform', 'translate(20,0)'); + .attr('transform', centerTransform); barpath.exit().remove(); barpath.each(function(d) { @@ -375,7 +380,7 @@ module.exports = function style(s, gd, legend) { pts.enter().append('path').classed('legendbox', true) // if we want the median bar, prepend M6,0H-6 .attr('d', 'M6,6H-6V-6H6Z') - .attr('transform', 'translate(20,0)'); + .attr('transform', centerTransform); pts.exit().remove(); pts.each(function() { @@ -415,7 +420,7 @@ module.exports = function style(s, gd, legend) { if(i) return 'M-15,0H-8M-8,6V-6H8Z'; // increasing return 'M15,0H8M8,-6V6H-8Z'; // decreasing }) - .attr('transform', 'translate(20,0)') + .attr('transform', centerTransform) .style('stroke-miterlimit', 1); pts.exit().remove(); @@ -442,7 +447,7 @@ module.exports = function style(s, gd, legend) { if(i) return 'M-15,0H0M-8,-6V0'; // increasing return 'M15,0H0M8,6V0'; // decreasing }) - .attr('transform', 'translate(20,0)') + .attr('transform', centerTransform) .style('stroke-miterlimit', 1); pts.exit().remove(); @@ -478,7 +483,7 @@ module.exports = function style(s, gd, legend) { .data(isVisible ? [d] : []); pts.enter().append('path').classed('legend' + desiredType, true) .attr('d', 'M6,6H-6V-6H6Z') - .attr('transform', 'translate(20,0)'); + .attr('transform', centerTransform); pts.exit().remove(); if(pts.size()) { @@ -576,7 +581,7 @@ module.exports = function style(s, gd, legend) { .selectAll('path.legend3dandfriends') .data(ptsData); pts.enter().append('path').classed('legend3dandfriends', true) - .attr('transform', 'translate(20,0)') + .attr('transform', centerTransform) .style('stroke-miterlimit', 1); pts.exit().remove(); diff --git a/test/image/baselines/legend_itemwidth_dashline.png b/test/image/baselines/legend_itemwidth_dashline.png new file mode 100644 index 00000000000..016e4aaa4d2 Binary files /dev/null and b/test/image/baselines/legend_itemwidth_dashline.png differ diff --git a/test/image/mocks/legend_itemwidth_dashline.json b/test/image/mocks/legend_itemwidth_dashline.json new file mode 100644 index 00000000000..000cd459e15 --- /dev/null +++ b/test/image/mocks/legend_itemwidth_dashline.json @@ -0,0 +1,45 @@ +{ + "data": [ + { + "x": [ + 2, + 3, + 4, + 5 + ], + "y": [ + 16, + 5, + 11, + 9 + ], + "line": { + "dash": "dashdot" + }, + "type": "scatter" + }, + { + "x": [ + 1, + 2, + 3, + 4 + ], + "y": [ + 12, + 9, + 15, + 12 + ], + "line": { + "dash": "dash" + }, + "type": "scatter" + } + ], + "layout": { + "legend": { + "itemwidth": 60 + } + } +} diff --git a/test/jasmine/tests/mock_test.js b/test/jasmine/tests/mock_test.js index 86825fc7e0d..a3e7cd1d6b6 100644 --- a/test/jasmine/tests/mock_test.js +++ b/test/jasmine/tests/mock_test.js @@ -669,6 +669,7 @@ var list = [ 'legend_horizontal_one_row', 'legend_horizontal_wrap-alll-lines', 'legend_inside', + 'legend_itemwidth_dashline', 'legend_labels', 'legend_large_margin', 'legend_margin-autoexpand-false', @@ -1733,6 +1734,7 @@ figs['legend_horizontal_groups'] = require('@mocks/legend_horizontal_groups'); figs['legend_horizontal_one_row'] = require('@mocks/legend_horizontal_one_row'); figs['legend_horizontal_wrap-alll-lines'] = require('@mocks/legend_horizontal_wrap-alll-lines'); figs['legend_inside'] = require('@mocks/legend_inside'); +figs['legend_itemwidth_dashline'] = require('@mocks/legend_itemwidth_dashline'); figs['legend_labels'] = require('@mocks/legend_labels'); figs['legend_large_margin'] = require('@mocks/legend_large_margin'); figs['legend_margin-autoexpand-false'] = require('@mocks/legend_margin-autoexpand-false');