diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index fbedb04763f..891e2387c4b 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -41,9 +41,9 @@ module.exports = function draw(gd) { if(!gd._legendMouseDownTime) gd._legendMouseDownTime = 0; - var opts = fullLayout.legend, - legendData = fullLayout.showlegend && getLegendData(gd.calcdata, opts), - hiddenSlices = fullLayout.hiddenlabels || []; + var opts = fullLayout.legend; + var legendData = fullLayout.showlegend && getLegendData(gd.calcdata, opts); + var hiddenSlices = fullLayout.hiddenlabels || []; if(!fullLayout.showlegend || !legendData.length) { fullLayout._infolayer.selectAll('.legend').remove(); @@ -53,6 +53,17 @@ module.exports = function draw(gd) { return; } + var maxLength = 0; + for(var i = 0; i < legendData.length; i++) { + for(var j = 0; j < legendData[i].length; j++) { + var item = legendData[i][j][0]; + var trace = item.trace; + var isPie = Registry.traceIs(trace, 'pie'); + var name = isPie ? item.label : trace.name; + maxLength = Math.max(maxLength, name && name.length || 0); + } + } + var firstRender = false; var legend = Lib.ensureSingle(fullLayout._infolayer, 'g', 'legend', function(s) { s.attr('pointer-events', 'all'); @@ -108,7 +119,7 @@ module.exports = function draw(gd) { }) .each(function() { d3.select(this) - .call(drawTexts, gd) + .call(drawTexts, gd, maxLength) .call(setupTraceToggle, gd); }); @@ -352,20 +363,21 @@ module.exports = function draw(gd) { } }; -function drawTexts(g, gd) { - var legendItem = g.data()[0][0], - fullLayout = gd._fullLayout, - trace = legendItem.trace, - isPie = Registry.traceIs(trace, 'pie'), - traceIndex = trace.index, - name = isPie ? legendItem.label : trace.name; +function drawTexts(g, gd, maxLength) { + var legendItem = g.data()[0][0]; + var fullLayout = gd._fullLayout; + var trace = legendItem.trace; + var isPie = Registry.traceIs(trace, 'pie'); + var traceIndex = trace.index; + var name = isPie ? legendItem.label : trace.name; + var isEditable = gd._context.edits.legendText && !isPie; - var text = Lib.ensureSingle(g, 'text', 'legendtext'); + var textEl = Lib.ensureSingle(g, 'text', 'legendtext'); - text.attr('text-anchor', 'start') + textEl.attr('text-anchor', 'start') .classed('user-select-none', true) .call(Drawing.font, fullLayout.legend.font) - .text(name); + .text(isEditable ? ensureLength(name, maxLength) : name); function textLayout(s) { svgTextUtils.convertToTspans(s, gd, function() { @@ -373,17 +385,13 @@ function drawTexts(g, gd) { }); } - if(gd._context.edits.legendText && !isPie) { - text.call(svgTextUtils.makeEditable, {gd: gd}) + if(isEditable) { + textEl.call(svgTextUtils.makeEditable, {gd: gd, text: name}) .call(textLayout) - .on('edit', function(text) { - this.text(text) + .on('edit', function(newName) { + this.text(ensureLength(newName, maxLength)) .call(textLayout); - var origText = text; - - if(!this.text()) text = ' \u0020\u0020 '; - var fullInput = legendItem.trace._fullInput || {}; var update = {}; @@ -393,24 +401,35 @@ function drawTexts(g, gd) { var kcont = Lib.keyedContainer(fullInput, 'transforms[' + index + '].styles', 'target', 'value.name'); - if(origText === '') { - kcont.remove(legendItem.trace._group); - } else { - kcont.set(legendItem.trace._group, text); - } + kcont.set(legendItem.trace._group, newName); update = kcont.constructUpdate(); } else { - update.name = text; + update.name = newName; } return Registry.call('restyle', gd, update, traceIndex); }); } else { - textLayout(text); + textLayout(textEl); } } +/* + * Make sure we have a reasonably clickable region. + * If this string is missing or very short, pad it with spaces out to at least + * 4 characters, up to the max length of other labels, on the assumption that + * most characters are wider than spaces so a string of spaces will usually be + * no wider than the real labels. + */ +function ensureLength(str, maxLength) { + var targetLength = Math.max(4, maxLength); + if(str && str.trim().length >= targetLength / 2) return str; + str = str || ''; + for(var i = targetLength - str.length; i > 0; i--) str += ' '; + return str; +} + function setupTraceToggle(g, gd) { var newMouseDownTime, numClicks = 1; diff --git a/src/lib/svg_text_utils.js b/src/lib/svg_text_utils.js index add53dcd3ed..6f08f91371b 100644 --- a/src/lib/svg_text_utils.js +++ b/src/lib/svg_text_utils.js @@ -607,6 +607,9 @@ exports.makeEditable = function(context, options) { var cStyle = context.node().style; var fontSize = parseFloat(cStyle.fontSize || 12); + var initialText = options.text; + if(initialText === undefined) initialText = context.attr('data-unformatted'); + div.classed('plugin-editable editable', true) .style({ position: 'absolute', @@ -621,7 +624,7 @@ exports.makeEditable = function(context, options) { 'box-sizing': 'border-box' }) .attr({contenteditable: true}) - .text(options.text || context.attr('data-unformatted')) + .text(initialText) .call(alignHTMLWith(context, container, options)) .on('blur', function() { gd._editing = false; diff --git a/src/transforms/groupby.js b/src/transforms/groupby.js index 53d253d4d2e..74ef3a12f8a 100644 --- a/src/transforms/groupby.js +++ b/src/transforms/groupby.js @@ -214,7 +214,7 @@ function transformOne(trace, state) { suppliedName = groupNameObj.get(groupName); } - if(suppliedName) { + if(suppliedName || suppliedName === '') { newTrace.name = suppliedName; } else { newTrace.name = Lib.templateString(opts.nameformat, { diff --git a/test/jasmine/tests/legend_test.js b/test/jasmine/tests/legend_test.js index eecef5461e9..940b5c940a2 100644 --- a/test/jasmine/tests/legend_test.js +++ b/test/jasmine/tests/legend_test.js @@ -920,11 +920,11 @@ describe('legend interaction', function() { x: [1, 2, 3], y: [5, 4, 3] }, { - x: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], - y: [1, 3, 2, 4, 3, 5, 4, 6, 5, 7, 6, 8, 7, 9], + x: [1, 2, 3, 4, 5, 6, 7, 8], + y: [1, 3, 2, 4, 3, 5, 4, 6], transforms: [{ type: 'groupby', - groups: [1, 2, 1, 2, 3, 4, 3, 4, 5, 6, 5, 6, 7, 8] + groups: [1, 2, 1, 2, 3, 4, 3, 4] }] }], config: {editable: true} @@ -947,35 +947,68 @@ describe('legend interaction', function() { }).then(delay(20)); } + function assertLabels(expected) { + var labels = []; + d3.selectAll('text.legendtext').each(function() { + labels.push(this.textContent); + }); + expect(labels).toEqual(expected); + } + it('sets and unsets trace group names', function(done) { + assertLabels(['trace 0', '1 (trace 1)', '2 (trace 1)', '3 (trace 1)', '4 (trace 1)']); // Set the name of the first trace: _setValue(0, 'foo').then(function() { expect(gd.data[0].name).toEqual('foo'); - }).then(function() { + // labels shorter than half the longest get padded with spaces to match the longest length + assertLabels(['foo ', '1 (trace 1)', '2 (trace 1)', '3 (trace 1)', '4 (trace 1)']); + // Set the name of the third legend item: - return _setValue(3, 'bar'); + return _setValue(3, 'barbar'); }).then(function() { expect(gd.data[1].transforms[0].styles).toEqual([ - {value: {name: 'bar'}, target: 3} + {value: {name: 'barbar'}, target: 3} ]); + assertLabels(['foo ', '1 (trace 1)', '2 (trace 1)', 'barbar', '4 (trace 1)']); + + return _setValue(2, 'asdf'); }).then(function() { - return _setValue(4, 'asdf'); + expect(gd.data[1].transforms[0].styles).toEqual([ + {value: {name: 'barbar'}, target: 3}, + {value: {name: 'asdf'}, target: 2} + ]); + assertLabels(['foo ', '1 (trace 1)', 'asdf ', 'barbar', '4 (trace 1)']); + + // Clear the group names: + return _setValue(3, ''); }).then(function() { + assertLabels(['foo ', '1 (trace 1)', 'asdf ', ' ', '4 (trace 1)']); + return _setValue(2, ''); + }).then(function() { + // Verify the group names have been cleared: expect(gd.data[1].transforms[0].styles).toEqual([ - {value: {name: 'bar'}, target: 3}, - {value: {name: 'asdf'}, target: 4} + {target: 3, value: {name: ''}}, + {target: 2, value: {name: ''}} ]); + assertLabels(['foo ', '1 (trace 1)', ' ', ' ', '4 (trace 1)']); + + return _setValue(0, ''); }).then(function() { - // Unset the group names: - return _setValue(3, ''); + expect(gd.data[0].name).toEqual(''); + assertLabels([' ', '1 (trace 1)', ' ', ' ', '4 (trace 1)']); + + return _setValue(0, 'boo~~~'); }).then(function() { - return _setValue(4, ''); + expect(gd.data[0].name).toEqual('boo~~~'); + assertLabels(['boo~~~', '1 (trace 1)', ' ', ' ', '4 (trace 1)']); + + return _setValue(2, 'hoo'); }).then(function() { - // Verify the group names have been cleaned up: expect(gd.data[1].transforms[0].styles).toEqual([ - {target: 3, value: {}}, - {target: 4, value: {}} + {target: 3, value: {name: ''}}, + {target: 2, value: {name: 'hoo'}} ]); + assertLabels(['boo~~~', '1 (trace 1)', 'hoo ', ' ', '4 (trace 1)']); }).catch(fail).then(done); }); });