diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index 6b6e783a68b..561022d82d6 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -393,20 +393,26 @@ function drawTexts(g, gd, legendObj) { var isEditable = !legendObj._inHover && gd._context.edits.legendText && !isPieLike; var maxNameLength = legendObj._maxNameLength; - var name; - if(!legendObj.entries) { - name = isPieLike ? legendItem.label : trace.name; - if(trace._meta) { - name = Lib.templateString(name, trace._meta); - } + var name, font; + if(legendItem.groupTitle) { + name = legendItem.groupTitle.text; + font = legendItem.groupTitle.font; } else { - name = legendItem.text; + font = legendObj.font; + if(!legendObj.entries) { + name = isPieLike ? legendItem.label : trace.name; + if(trace._meta) { + name = Lib.templateString(name, trace._meta); + } + } else { + name = legendItem.text; + } } var textEl = Lib.ensureSingle(g, 'text', 'legendtext'); textEl.attr('text-anchor', 'start') - .call(Drawing.font, legendObj.font) + .call(Drawing.font, font) .text(isEditable ? ensureLength(name, maxNameLength) : name); var textGap = legendObj.itemwidth + constants.itemGap * 2; @@ -512,7 +518,15 @@ function computeTextDimensions(g, gd, legendObj, aTitle) { var mathjaxNode = mathjaxGroup.node(); if(!legendObj) legendObj = gd._fullLayout.legend; var bw = legendObj.borderwidth; - var lineHeight = (aTitle === MAIN_TITLE ? legendObj.title : legendObj).font.size * LINE_SPACING; + var font; + if(aTitle === MAIN_TITLE) { + font = legendObj.title.font; + } else if(legendItem.groupTitle) { + font = legendItem.groupTitle.font; + } else { + font = legendObj.font; + } + var lineHeight = font.size * LINE_SPACING; var height, width; if(mathjaxNode) { @@ -549,8 +563,14 @@ function computeTextDimensions(g, gd, legendObj, aTitle) { bw + lineHeight ); } else { // legend item + var x = constants.itemGap * 2 + legendObj.itemwidth; + if(legendItem.groupTitle) { + x = constants.itemGap; + width -= legendObj.itemwidth; + } + svgTextUtils.positionText(textEl, - legendObj.itemwidth + constants.itemGap * 2, + x, -lineHeight * ((textLines - 1) / 2 - 0.3) ); } diff --git a/src/components/legend/get_legend_data.js b/src/components/legend/get_legend_data.js index f5a7b19fbbb..7d236ae7617 100644 --- a/src/components/legend/get_legend_data.js +++ b/src/components/legend/get_legend_data.js @@ -117,8 +117,33 @@ module.exports = function getLegendData(calcdata, opts) { // sort considering trace.legendrank and legend.traceorder legendData[i].forEach(function(a, k) { a._preSort = k; }); legendData[i].sort(orderFn2); + + var firstItem = legendData[i][0]; + + var groupTitle = null; + // get group title text + for(j = 0; j < legendData[i].length; j++) { + var gt = legendData[i][j].trace.legendgrouptitle; + if(gt && gt.text) { + groupTitle = gt; + break; + } + } + + // reverse order if(reversed) legendData[i].reverse(); + if(groupTitle) { + // set group title text + legendData[i].unshift({ + i: -1, + groupTitle: groupTitle, + trace: { + showlegend: firstItem.trace.showlegend + } + }); + } + // rearrange lgroupToTraces into a d3-friendly array of arrays for(j = 0; j < legendData[i].length; j++) { legendData[i][j] = [ diff --git a/src/components/legend/handle_click.js b/src/components/legend/handle_click.js index b8683060a68..8c33fb975de 100644 --- a/src/components/legend/handle_click.js +++ b/src/components/legend/handle_click.js @@ -32,6 +32,8 @@ module.exports = function handleClick(g, gd, numClicks) { []; var legendItem = g.data()[0][0]; + if(legendItem.groupTitle) return; // no click on group legends for now + var fullData = gd._fullData; var fullTrace = legendItem.trace; var legendgroup = fullTrace.legendgroup; diff --git a/src/plots/attributes.js b/src/plots/attributes.js index 2b819ea95a3..f90582ec200 100644 --- a/src/plots/attributes.js +++ b/src/plots/attributes.js @@ -1,5 +1,6 @@ 'use strict'; +var fontAttrs = require('./font_attributes'); var fxAttrs = require('../components/fx/attributes'); module.exports = { @@ -41,6 +42,23 @@ module.exports = { 'when toggling legend items.' ].join(' ') }, + legendgrouptitle: { + text: { + valType: 'string', + dflt: '', + editType: 'style', + description: [ + 'Sets the title of the legend group.' + ].join(' ') + }, + font: fontAttrs({ + editType: 'style', + description: [ + 'Sets this legend group\'s title font.' + ].join(' '), + }), + editType: 'style', + }, legendrank: { valType: 'number', dflt: 1000, diff --git a/src/plots/plots.js b/src/plots/plots.js index 9a51b1f201b..b0c4fbfce90 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -1309,6 +1309,13 @@ plots.supplyTraceDefaults = function(traceIn, traceOut, colorIndex, layout, trac ); coerce('legendgroup'); + var titleText = coerce('legendgrouptitle.text'); + if(titleText) { + Lib.coerceFont(coerce, 'legendgrouptitle.font', Lib.extendFlat({}, layout.font, { + size: Math.round(layout.font.size * 1.1) // default to larger font size + })); + } + coerce('legendrank'); traceOut._dfltShowLegend = true; diff --git a/test/image/baselines/legendgroup-titles-pie.png b/test/image/baselines/legendgroup-titles-pie.png new file mode 100644 index 00000000000..8decf457411 Binary files /dev/null and b/test/image/baselines/legendgroup-titles-pie.png differ diff --git a/test/image/baselines/legendgroup-titles.png b/test/image/baselines/legendgroup-titles.png new file mode 100644 index 00000000000..22087afe5b2 Binary files /dev/null and b/test/image/baselines/legendgroup-titles.png differ diff --git a/test/image/baselines/legendrank.png b/test/image/baselines/legendrank.png index 2f5d879126e..cbbf279fbca 100644 Binary files a/test/image/baselines/legendrank.png and b/test/image/baselines/legendrank.png differ diff --git a/test/image/baselines/legendrank2.png b/test/image/baselines/legendrank2.png index 35261255629..7601f6a9e39 100644 Binary files a/test/image/baselines/legendrank2.png and b/test/image/baselines/legendrank2.png differ diff --git a/test/image/mocks/legendgroup-titles-pie.json b/test/image/mocks/legendgroup-titles-pie.json new file mode 100644 index 00000000000..7d586a10d94 --- /dev/null +++ b/test/image/mocks/legendgroup-titles-pie.json @@ -0,0 +1,73 @@ +{ + "data": [ + { + "type": "pie", + "domain": { + "y": [0.8, 1] + }, + "labels": [0], + "legendgroup": "G1" + }, + { + "type": "pie", + "domain": { + "y": [0.6, 0.8] + }, + "labels": [1], + "legendgroup": "G1" + }, + { + "type": "pie", + "domain": { + "y": [0.4, 0.6] + }, + "labels": [2], + "legendgroup": "G1", + "legendgrouptitle": { + "text": "First group" + } + }, + { + "type": "pie", + "domain": { + "y": [0.2, 0.4] + }, + "labels": [3], + "legendgroup": "G2", + "legendgrouptitle": { + "text": "Second group" + } + }, + { + "domain": { + "y": [0, 0.2] + }, + "type": "pie", + "labels": [4, 5], + "legendgroup": "G3", + "legendgrouptitle": { + "text": "Third group" + } + } + ], + "layout": { + "title": { + "text": "legend group titles" + }, + "margin": { + "t": 50, + "b": 25, + "l": 25, + "r": 25 + }, + "width": 300, + "height": 300, + "legend": { + "bgcolor": "lightblue", + "title": { + "text": "" + } + }, + "hovermode": "x unified" + } +} diff --git a/test/image/mocks/legendgroup-titles.json b/test/image/mocks/legendgroup-titles.json new file mode 100644 index 00000000000..d4272e0230f --- /dev/null +++ b/test/image/mocks/legendgroup-titles.json @@ -0,0 +1,64 @@ +{ + "data": [ + { + "y": [0], + "legendgroup": "G1" + }, + { + "y": [1], + "legendgroup": "G1" + }, + { + "y": [2], + "legendgroup": "G1", + "legendgrouptitle": { + "text": "First group" + } + }, + { + "y": [3], + "legendgroup": "G2", + "legendgrouptitle": { + "text": "Second
group", + "font": { + "family": "Raleway", + "size": 14 + } + } + }, + { + "y": [4], + "legendgroup": "G3", + "legendgrouptitle": { + "text": "Third group" + } + }, + { + "y": [5], + "legendgroup": "G3" + } + ], + "layout": { + "title": { + "text": "legend group titles" + }, + "margin": { + "t": 50, + "b": 25, + "l": 25, + "r": 25 + }, + "width": 300, + "height": 300, + "yaxis": { + "autorange": "reversed" + }, + "legend": { + "bgcolor": "lightblue", + "title": { + "text": "" + } + }, + "hovermode": "x unified" + } +} diff --git a/test/image/mocks/legendrank.json b/test/image/mocks/legendrank.json index 182b9c7afae..d7d755a648a 100644 --- a/test/image/mocks/legendrank.json +++ b/test/image/mocks/legendrank.json @@ -4,6 +4,7 @@ { "legendrank": 2, "legendgroup": "pie", + "legendgrouptitle": { "text": "Next pie" }, "type": "pie", "labels": ["a","b","c","c","c","a"], "textinfo": "none", @@ -15,6 +16,7 @@ { "legendrank": 1, "legendgroup": "pie", + "legendgrouptitle": { "text": "Pie" }, "type": "pie", "labels": ["z","x","x","x","y", "y"], "sort": false, @@ -25,9 +27,9 @@ } }, {"type": "scatter", "name": "2", "y": [2], "yaxis": "y", "legendgroup": "one", "legendrank": 2}, - {"type": "scatter", "name": "1", "y": [1], "yaxis": "y", "legendgroup": "one", "legendrank": 1}, - {"type": "bar", "name": "2", "y": [2], "yaxis": "y2", "legendgroup": "two", "legendrank": 2}, - {"type": "scatter", "name": "3", "y": [3], "yaxis": "y", "legendgroup": "one", "legendrank": 3}, + {"type": "scatter", "name": "1", "y": [1], "yaxis": "y", "legendgroup": "one", "legendrank": 1, "legendgrouptitle": { "text": "Down" }}, + {"type": "bar", "name": "2", "y": [2], "yaxis": "y2", "legendgroup": "two", "legendrank": 2, "legendgrouptitle": { "text": "Up" }}, + {"type": "scatter", "name": "3", "y": [3], "yaxis": "y", "legendgroup": "one", "legendrank": 3, "legendgrouptitle": { "text": "IGNORE ME!" }}, {"type": "bar", "name": "3", "y": [3], "yaxis": "y2", "legendgroup": "two", "legendrank": 1} ], "layout": { @@ -36,7 +38,8 @@ }, "hovermode": "x unified", "margin": { - "t": 50 + "t": 50, + "b": 25 }, "width": 300, "height": 400, diff --git a/test/image/mocks/legendrank2.json b/test/image/mocks/legendrank2.json index 5cb28a72483..773983ab87e 100644 --- a/test/image/mocks/legendrank2.json +++ b/test/image/mocks/legendrank2.json @@ -3,31 +3,36 @@ { "name": "A", "legendrank": 2, - "y": [-2] + "y": [-2], + "legendgrouptitle": { "text": "Middle" } }, { "name": "D", "legendrank": 4, "y": [-4], - "legendgroup": "bottom" + "legendgroup": "bottom", + "legendgrouptitle": { "text": "Bottom" } }, { "name": "E", "legendrank": 4, "y": [-4], - "legendgroup": "bottom" + "legendgroup": "bottom", + "legendgrouptitle": { "text": "Second" } }, { "name": "B", "legendrank": 1, "y": [-1], - "legendgroup": "top" + "legendgroup": "top", + "legendgrouptitle": { "text": "Top" } }, { "name": "C", "legendrank": 3, "y": [-3], - "legendgroup": "top" + "legendgroup": "top", + "legendgrouptitle": { "text": "Not first" } } ], "layout": {