diff --git a/src/components/drawing/index.js b/src/components/drawing/index.js index 05a56873b1d..1cbad7ce154 100644 --- a/src/components/drawing/index.js +++ b/src/components/drawing/index.js @@ -136,7 +136,7 @@ drawing.crispRound = function(gd, lineWidth, dflt) { drawing.singleLineStyle = function(d, s, lw, lc, ld) { s.style('fill', 'none'); var line = (((d || [])[0] || {}).trace || {}).line || {}; - var lw1 = lw || line.width||0; + var lw1 = lw || line.width || 0; var dash = ld || line.dash || ''; Color.stroke(s, lc || line.color); @@ -147,7 +147,7 @@ drawing.lineGroupStyle = function(s, lw, lc, ld) { s.style('fill', 'none') .each(function(d) { var line = (((d || [])[0] || {}).trace || {}).line || {}; - var lw1 = lw || line.width||0; + var lw1 = lw || line.width || 0; var dash = ld || line.dash || ''; d3.select(this) diff --git a/src/components/legend/attributes.js b/src/components/legend/attributes.js index 4ef4f10edcc..1a11eda29c9 100644 --- a/src/components/legend/attributes.js +++ b/src/components/legend/attributes.js @@ -78,6 +78,18 @@ module.exports = { 'Sets the amount of vertical space (in px) between legend groups.' ].join(' ') }, + itemsizing: { + valType: 'enumerated', + values: ['trace', 'constant'], + dflt: 'trace', + role: 'style', + editType: 'legend', + description: [ + 'Determines if the legend items symbols scale with their corresponding *trace* attributes', + 'or remain *constant* independent of the symbol size on the graph.' + ].join(' ') + }, + x: { valType: 'number', min: -2, diff --git a/src/components/legend/defaults.js b/src/components/legend/defaults.js index 41772c1ed38..bb69af92f4b 100644 --- a/src/components/legend/defaults.js +++ b/src/components/legend/defaults.js @@ -103,6 +103,8 @@ module.exports = function legendDefaults(layoutIn, layoutOut, fullData) { coerce('traceorder', defaultOrder); if(helpers.isGrouped(layoutOut.legend)) coerce('tracegroupgap'); + coerce('itemsizing'); + coerce('x', defaultX); coerce('xanchor', defaultXAnchor); coerce('y', defaultY); diff --git a/src/components/legend/style.js b/src/components/legend/style.js index cd373033313..3b3c3c6c0f7 100644 --- a/src/components/legend/style.js +++ b/src/components/legend/style.js @@ -17,16 +17,38 @@ var Color = require('../color'); var subTypes = require('../../traces/scatter/subtypes'); var stylePie = require('../../traces/pie/style_one'); +var pieCastOption = require('../../traces/pie/helpers').castOption; + +var CST_MARKER_SIZE = 12; +var CST_LINE_WIDTH = 5; +var CST_MARKER_LINE_WIDTH = 2; +var MAX_LINE_WIDTH = 10; +var MAX_MARKER_LINE_WIDTH = 5; module.exports = function style(s, gd) { + var fullLayout = gd._fullLayout; + var legend = fullLayout.legend; + var constantItemSizing = legend.itemsizing === 'constant'; + + function boundLineWidth(mlw, cont, max, cst) { + var v; + if(mlw + 1) { + v = mlw; + } else if(cont && cont.width > 0) { + v = cont.width; + } else { + return 0; + } + return constantItemSizing ? cst : Math.min(v, max); + } + s.each(function(d) { var traceGroup = d3.select(this); var layers = Lib.ensureSingle(traceGroup, 'g', 'layers'); layers.style('opacity', d[0].trace.opacity); - // Marker vertical alignment - var valign = gd._fullLayout.legend.valign; + var valign = legend.valign; var lineHeight = d[0].lineHeight; var height = d[0].height; @@ -71,28 +93,27 @@ module.exports = function style(s, gd) { .each(styleOHLC); function styleLines(d) { - var trace = d[0].trace; + var d0 = d[0]; + var trace = d0.trace; var showFill = trace.visible && trace.fill && trace.fill !== 'none'; var showLine = subTypes.hasLines(trace); var contours = trace.contours; var showGradientLine = false; var showGradientFill = false; + var dMod, tMod; if(contours) { var coloring = contours.coloring; if(coloring === 'lines') { showGradientLine = true; - } - else { - showLine = coloring === 'none' || coloring === 'heatmap' || - contours.showlines; + } else { + showLine = coloring === 'none' || coloring === 'heatmap' || contours.showlines; } if(contours.type === 'constraint') { showFill = contours._operation !== '='; - } - else if(coloring === 'fill' || coloring === 'heatmap') { + } else if(coloring === 'fill' || coloring === 'heatmap') { showGradientFill = true; } } @@ -116,8 +137,14 @@ module.exports = function style(s, gd) { fill.attr('d', pathStart + 'h30v6h-30z') .call(showFill ? Drawing.fillGroupStyle : fillGradient); + if(showLine || showGradientLine) { + var lw = boundLineWidth(undefined, trace.line, MAX_LINE_WIDTH, CST_LINE_WIDTH); + tMod = Lib.minExtend(trace, {line: {width: lw}}); + dMod = [Lib.minExtend(d0, {trace: tMod})]; + } + var line = this3.select('.legendlines').selectAll('path') - .data(showLine || showGradientLine ? [d] : []); + .data(showLine || showGradientLine ? [dMod] : []); line.enter().append('path').classed('js-line', true); line.exit().remove(); @@ -159,12 +186,16 @@ module.exports = function style(s, gd) { // 'scatter3d' don't use gd.calcdata, // use d0.trace to infer arrayOk attributes - function boundVal(attrIn, arrayToValFn, bounds) { + function boundVal(attrIn, arrayToValFn, bounds, cst) { var valIn = Lib.nestedProperty(trace, attrIn).get(); var valToBound = (Lib.isArrayOrTypedArray(valIn) && arrayToValFn) ? arrayToValFn(valIn) : valIn; + if(constantItemSizing && valToBound && cst !== undefined) { + valToBound = cst; + } + if(bounds) { if(valToBound < bounds[0]) return bounds[0]; else if(valToBound > bounds[1]) return bounds[1]; @@ -184,21 +215,21 @@ module.exports = function style(s, gd) { dEdit.mx = boundVal('marker.symbol', pickFirst); dEdit.mo = boundVal('marker.opacity', Lib.mean, [0.2, 1]); dEdit.mlc = boundVal('marker.line.color', pickFirst); - dEdit.mlw = boundVal('marker.line.width', Lib.mean, [0, 5]); + dEdit.mlw = boundVal('marker.line.width', Lib.mean, [0, 5], CST_MARKER_LINE_WIDTH); tEdit.marker = { sizeref: 1, sizemin: 1, sizemode: 'diameter' }; - var ms = boundVal('marker.size', Lib.mean, [2, 16]); + var ms = boundVal('marker.size', Lib.mean, [2, 16], CST_MARKER_SIZE); dEdit.ms = ms; tEdit.marker.size = ms; } if(showLines) { tEdit.line = { - width: boundVal('line.width', pickFirst, [0, 10]) + width: boundVal('line.width', pickFirst, [0, 10], CST_LINE_WIDTH) }; } @@ -262,12 +293,13 @@ module.exports = function style(s, gd) { pts.each(function(dd) { var pt = d3.select(this); var cont = trace[dd[0]].marker; + var lw = boundLineWidth(undefined, cont.line, MAX_MARKER_LINE_WIDTH, CST_MARKER_LINE_WIDTH); pt.attr('d', dd[1]) - .style('stroke-width', cont.line.width + 'px') + .style('stroke-width', lw + 'px') .call(Color.fill, cont.color); - if(cont.line.width) { + if(lw) { pt.call(Color.stroke, cont.line.color); } }); @@ -289,14 +321,12 @@ module.exports = function style(s, gd) { barpath.each(function(d) { var p = d3.select(this); var d0 = d[0]; - var w = (d0.mlw + 1 || markerLine.width + 1) - 1; + var w = boundLineWidth(d0.mlw, marker.line, MAX_MARKER_LINE_WIDTH, CST_MARKER_LINE_WIDTH); p.style('stroke-width', w + 'px') .call(Color.fill, d0.mc || marker.color); - if(w) { - p.call(Color.stroke, d0.mlc || markerLine.color); - } + if(w) Color.stroke(p, d0.mlc || markerLine.color); }); } @@ -313,15 +343,13 @@ module.exports = function style(s, gd) { pts.exit().remove(); pts.each(function() { - var w = trace.line.width; var p = d3.select(this); + var w = boundLineWidth(undefined, trace.line, MAX_MARKER_LINE_WIDTH, CST_MARKER_LINE_WIDTH); p.style('stroke-width', w + 'px') .call(Color.fill, trace.fillcolor); - if(w) { - Color.stroke(p, trace.line.color); - } + if(w) Color.stroke(p, trace.line.color); }); } @@ -341,16 +369,14 @@ module.exports = function style(s, gd) { pts.exit().remove(); pts.each(function(_, i) { - var container = trace[i ? 'increasing' : 'decreasing']; - var w = container.line.width; var p = d3.select(this); + var cont = trace[i ? 'increasing' : 'decreasing']; + var w = boundLineWidth(undefined, cont.line, MAX_MARKER_LINE_WIDTH, CST_MARKER_LINE_WIDTH); p.style('stroke-width', w + 'px') - .call(Color.fill, container.fillcolor); + .call(Color.fill, cont.fillcolor); - if(w) { - Color.stroke(p, container.line.color); - } + if(w) Color.stroke(p, cont.line.color); }); } @@ -370,21 +396,20 @@ module.exports = function style(s, gd) { pts.exit().remove(); pts.each(function(_, i) { - var container = trace[i ? 'increasing' : 'decreasing']; - var w = container.line.width; var p = d3.select(this); + var cont = trace[i ? 'increasing' : 'decreasing']; + var w = boundLineWidth(undefined, cont.line, MAX_MARKER_LINE_WIDTH, CST_MARKER_LINE_WIDTH); p.style('fill', 'none') - .call(Drawing.dashLine, container.line.dash, w); + .call(Drawing.dashLine, cont.line.dash, w); - if(w) { - Color.stroke(p, container.line.color); - } + if(w) Color.stroke(p, cont.line.color); }); } function stylePies(d) { - var trace = d[0].trace; + var d0 = d[0]; + var trace = d0.trace; var pts = d3.select(this).select('g.legendpoints') .selectAll('path.legendpie') @@ -394,6 +419,12 @@ module.exports = function style(s, gd) { .attr('transform', 'translate(20,0)'); pts.exit().remove(); - if(pts.size()) pts.call(stylePie, d[0], trace); + if(pts.size()) { + var cont = (trace.marker || {}).line; + var lw = boundLineWidth(pieCastOption(cont.width, d0.pts), cont, MAX_MARKER_LINE_WIDTH, CST_MARKER_LINE_WIDTH); + var tMod = Lib.minExtend(trace, {marker: {line: {width: lw}}}); + var d0Mod = Lib.minExtend(d0, {trace: tMod}); + stylePie(pts, d0Mod, tMod); + } } }; diff --git a/src/traces/pie/style_one.js b/src/traces/pie/style_one.js index c981e3b9fb5..c0bd60d367f 100644 --- a/src/traces/pie/style_one.js +++ b/src/traces/pie/style_one.js @@ -16,7 +16,7 @@ module.exports = function styleOne(s, pt, trace) { var lineColor = castOption(line.color, pt.pts) || Color.defaultLine; var lineWidth = castOption(line.width, pt.pts) || 0; - s.style({'stroke-width': lineWidth}) + s.style('stroke-width', lineWidth) .call(Color.fill, pt.color) .call(Color.stroke, lineColor); }; diff --git a/test/image/baselines/gl3d_line_rectangle_render.png b/test/image/baselines/gl3d_line_rectangle_render.png index 35a1e9c8cca..79cd1df4a5a 100644 Binary files a/test/image/baselines/gl3d_line_rectangle_render.png and b/test/image/baselines/gl3d_line_rectangle_render.png differ diff --git a/test/image/baselines/gl3d_log-axis-big.png b/test/image/baselines/gl3d_log-axis-big.png index 5fecfae721c..e6644ca9889 100644 Binary files a/test/image/baselines/gl3d_log-axis-big.png and b/test/image/baselines/gl3d_log-axis-big.png differ diff --git a/test/image/baselines/gl3d_log-axis.png b/test/image/baselines/gl3d_log-axis.png index 873c58886e9..1fb6e58a631 100644 Binary files a/test/image/baselines/gl3d_log-axis.png and b/test/image/baselines/gl3d_log-axis.png differ diff --git a/test/image/baselines/legend-constant-itemsizing.png b/test/image/baselines/legend-constant-itemsizing.png new file mode 100644 index 00000000000..1b662b7081f Binary files /dev/null and b/test/image/baselines/legend-constant-itemsizing.png differ diff --git a/test/image/mocks/legend-constant-itemsizing.json b/test/image/mocks/legend-constant-itemsizing.json new file mode 100644 index 00000000000..efccd2fa0fc --- /dev/null +++ b/test/image/mocks/legend-constant-itemsizing.json @@ -0,0 +1,170 @@ +{ + "data": [ + { + "mode": "markers", + "name": "markers", + "y": [ 1, 1, 1 ], + "marker": { + "size": [ 20, 40, 30 ], + "line": { + "width": 10, + "color": "#444" + } + } + }, + { + "type": "bar", + "name": "bar", + "base": 2, + "y": [ 0.5, 0.5, 0.5 ], + "marker": { + "line": { + "width": 15 + } + } + }, + { + "mode": "lines", + "name": "lines", + "y": [ 3, 3, 3 ], + "line": { + "width": 30 + } + }, + { + "mode": "markers+lines+text", + "name": "all modes", + "y": [ 4, 4, 4 ], + "text": [ "a", "b", "c" ], + "textposition": "bottom left", + "line": { + "width": 20 + }, + "marker": { + "size": 50, + "line": { + "width": [ 10, 15, 14 ], + "color": "#444" + } + } + }, + { + "type": "waterfall", + "name": "waterfall", + "base": 5, + "y": [ 1, -0.5, null ], + "measure": ["", "", "total"], + "connector": { + "visible": false + }, + "increasing": { + "marker": { + "line": { + "width": 20 + } + } + }, + "decreasing": { + "marker": { + "line": { + "width": 10 + } + } + } + }, + { + "type": "ohlc", + "name": "ohlc", + "open": [ 7, 7, 7 ], + "high": [ 7.5, 7.5, 7.5 ], + "low": [ 6.5, 6.5, 6.5 ], + "close": [ 7.1, 6.9, 7.1 ], + "tickwidth": 0.1, + "increasing": { + "line": { + "width": 12 + } + }, + "decreasing": { + "line": { + "width": 13 + } + } + }, + { + "type": "candlestick", + "name": "candlestick", + "x": [ 0.5, 1.5 ], + "open": [ 7, 7 ], + "high": [ 7.5, 7.5 ], + "low": [ 6.5, 6.5 ], + "close": [ 7.4, 6.6 ], + "increasing": { + "line": { + "width": 15 + } + }, + "decreasing": { + "line": { + "width": 0 + } + } + }, + { + "type": "pie", + "name": "pie", + "labels": [ "pie" ], + "marker": { + "line": { + "width": 20 + } + }, + "domain": { + "y": [ 0.92, 1 ] + }, + "textinfo": "none" + } + ], + "layout": { + "xaxis": { + "visible": false, + "rangeslider": { + "visible": false + } + }, + "yaxis": { + "visible": false, + "domain": [ + 0, + 0.9 + ] + }, + "showlegend": true, + "legend": { + "x": 0, + "xanchor": "right", + "y": 0.5, + "yanchor": "center", + "itemsizing": "constant", + "traceorder": "reversed" + }, + "margin": { + "t": 10, + "b": 10, + "r": 10 + }, + "width": 600, + "height": 450, + "title": { + "text": "legend.itemsizing: constant", + "font": { + "size": 20 + }, + "x": 0.05, + "xanchor": "left", + "xref": "cont", + "y": 0.95, + "yanchor": "top" + } + } +} diff --git a/test/jasmine/tests/pie_test.js b/test/jasmine/tests/pie_test.js index 1d0c29d2629..85c29f319e3 100644 --- a/test/jasmine/tests/pie_test.js +++ b/test/jasmine/tests/pie_test.js @@ -494,11 +494,11 @@ describe('Pie traces', function() { .then(function() { var expWidths = ['3', '0', '0']; - d3.selectAll(SLICES_SELECTOR).each(function(d) { - expect(this.style.strokeWidth).toBe(expWidths[d.pointNumber]); + d3.selectAll(SLICES_SELECTOR).each(function(d, i) { + expect(this.style.strokeWidth).toBe(expWidths[d.pointNumber], 'sector #' + i); }); - d3.selectAll(LEGEND_ENTRIES_SELECTOR).each(function(d) { - expect(this.style.strokeWidth).toBe(expWidths[d[0].i]); + d3.selectAll(LEGEND_ENTRIES_SELECTOR).each(function(d, i) { + expect(this.style.strokeWidth).toBe(expWidths[d[0].i], 'item #' + i); }); }) .catch(failTest)