diff --git a/docs/configuration/legend.md b/docs/configuration/legend.md index d2a3a88b3b6..da087f86453 100644 --- a/docs/configuration/legend.md +++ b/docs/configuration/legend.md @@ -7,7 +7,7 @@ The legend configuration is passed into the `options.legend` namespace. The glob | Name | Type | Default | Description | -----| ---- | --------| ----------- -| `display` | `Boolean` | `true` | is the legend shown +| `display` | `Boolean` | `true` | Whether the legend is shown | `position` | `String` | `'top'` | Position of the legend. [more...](#position) | `fullWidth` | `Boolean` | `true` | Marks that this box should take the full width of the canvas (pushing down other boxes). This is unlikely to need to be changed in day-to-day use. | `onClick` | `Function` | | A callback that is called when a click event is registered on a label item @@ -28,15 +28,28 @@ The legend label configuration is nested below the legend configuration using th | Name | Type | Default | Description | -----| ---- | --------| ----------- -| `boxWidth` | `Number` | `40` | width of coloured box -| `fontSize` | `Number` | `12` | font size of text -| `fontStyle` | `String` | `'normal'` | font style of text +| `boxWidth` | `Number` | `40` | Width of coloured box +| `fontSize` | `Number` | `12` | Font size of text +| `fontStyle` | `String` | `'normal'` | Font style of text | `fontColor` | `Color` | `'#666'` | Color of text | `fontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | Font family of legend text. | `padding` | `Number` | `10` | Padding between rows of colored boxes. | `generateLabels` | `Function` | | Generates legend items for each thing in the legend. Default implementation returns the text + styling for the color box. See [Legend Item](#legend-item-interface) for details. | `filter` | `Function` | `null` | Filters legend items out of the legend. Receives 2 parameters, a [Legend Item](#legend-item-interface) and the chart data. -| `usePointStyle` | `Boolean` | `false` | Label style will match corresponding point style (size is based on fontSize, boxWidth is not used in this case). +| `style` | `String` | | Style of the label item. [more...](#label-style) + +### Label Style + +Possible label style values are: +* `'box'` +* `'line'` +* `'point'` + +`'box'` will draw a box using the background color, border width and color of the corresponding element. +`'line'` will draw a line using the corresponding line style. +`'point'` will make the label style match the corresponding point style (size is based on `fontSize`, `boxWidth` is not used in this case). + +If not set, the `'line'` style is used for line elements, and the `'box'` style for other elements. ## Legend Item Interface @@ -53,26 +66,29 @@ Items passed to the legend `onClick` function are the ones returned from `labels // If true, this item represents a hidden dataset. Label will be rendered with a strike-through effect hidden: Boolean, - // For box border. See https://developer.mozilla.org/en/docs/Web/API/CanvasRenderingContext2D/lineCap + // For line border. See https://developer.mozilla.org/en/docs/Web/API/CanvasRenderingContext2D/lineCap lineCap: String, - // For box border. See https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash + // For line border. See https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash lineDash: Array[Number], - // For box border. See https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset + // For line border. See https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset lineDashOffset: Number, - // For box border. See https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin + // For line border. See https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin lineJoin: String, - // Width of box border + // Width of box border or line lineWidth: Number, - // Stroke style of the legend box - strokeStyle: Color + // Stroke style of the legend box or line + strokeStyle: Color, - // Point style of the legend box (only used if usePointStyle is true) - pointStyle: String + // Point style of the legend box (only used if style is 'point') + pointStyle: String, + + // Style of the legend box + style: String } ``` @@ -91,7 +107,7 @@ var chart = new Chart(ctx, { fontColor: 'rgb(255, 99, 132)' } } -} + } }); ``` @@ -107,7 +123,7 @@ function(e, legendItem) { var meta = ci.getDatasetMeta(index); // See controller.isDatasetVisible comment - meta.hidden = meta.hidden === null? !ci.data.datasets[index].hidden : null; + meta.hidden = meta.hidden === null ? !ci.data.datasets[index].hidden : null; // We hid a dataset ... rerender the chart ci.update(); @@ -118,7 +134,7 @@ Lets say we wanted instead to link the display of the first two datasets. We cou ```javascript var defaultLegendClickHandler = Chart.defaults.global.legend.onClick; -var newLegendClickHandler = function (e, legendItem) { +var newLegendClickHandler = function(e, legendItem) { var index = legendItem.datasetIndex; if (index > 1) { @@ -128,11 +144,11 @@ var newLegendClickHandler = function (e, legendItem) { let ci = this.chart; [ci.getDatasetMeta(0), ci.getDatasetMeta(1)].forEach(function(meta) { - meta.hidden = meta.hidden === null? !ci.data.datasets[index].hidden : null; + meta.hidden = meta.hidden === null ? !ci.data.datasets[index].hidden : null; }); ci.update(); } -}; +}); var chart = new Chart(ctx, { type: 'line', diff --git a/src/controllers/controller.doughnut.js b/src/controllers/controller.doughnut.js index eb759fe60b8..1815b3455a1 100644 --- a/src/controllers/controller.doughnut.js +++ b/src/controllers/controller.doughnut.js @@ -50,6 +50,8 @@ defaults._set('doughnut', { var fill = custom.backgroundColor ? custom.backgroundColor : valueAtIndexOrDefault(ds.backgroundColor, i, arcOpts.backgroundColor); var stroke = custom.borderColor ? custom.borderColor : valueAtIndexOrDefault(ds.borderColor, i, arcOpts.borderColor); var bw = custom.borderWidth ? custom.borderWidth : valueAtIndexOrDefault(ds.borderWidth, i, arcOpts.borderWidth); + var labelOpts = chart.options.legend.labels; + var style = labelOpts.style; return { text: label, @@ -57,6 +59,8 @@ defaults._set('doughnut', { strokeStyle: stroke, lineWidth: bw, hidden: isNaN(ds.data[i]) || meta.data[i].hidden, + // `usePointStyle` is deprecated. To be removed at version 3 + style: !style && labelOpts.usePointStyle ? 'point' : style, // Extra data used for toggling the correct item index: i diff --git a/src/controllers/controller.polarArea.js b/src/controllers/controller.polarArea.js index 663a9534d55..80cb46ceca5 100644 --- a/src/controllers/controller.polarArea.js +++ b/src/controllers/controller.polarArea.js @@ -64,6 +64,8 @@ defaults._set('polarArea', { var fill = custom.backgroundColor ? custom.backgroundColor : valueAtIndexOrDefault(ds.backgroundColor, i, arcOpts.backgroundColor); var stroke = custom.borderColor ? custom.borderColor : valueAtIndexOrDefault(ds.borderColor, i, arcOpts.borderColor); var bw = custom.borderWidth ? custom.borderWidth : valueAtIndexOrDefault(ds.borderWidth, i, arcOpts.borderWidth); + var labelOpts = chart.options.legend.labels; + var style = labelOpts.style; return { text: label, @@ -71,6 +73,8 @@ defaults._set('polarArea', { strokeStyle: stroke, lineWidth: bw, hidden: isNaN(ds.data[i]) || meta.data[i].hidden, + // `usePointStyle` is deprecated. To be removed at version 3 + style: !style && labelOpts.usePointStyle ? 'point' : style, // Extra data used for toggling the correct item index: i diff --git a/src/plugins/plugin.legend.js b/src/plugins/plugin.legend.js index 3f1559c3003..2cad9020b83 100644 --- a/src/plugins/plugin.legend.js +++ b/src/plugins/plugin.legend.js @@ -47,6 +47,19 @@ defaults._set('global', { generateLabels: function(chart) { var data = chart.data; return helpers.isArray(data.datasets) ? data.datasets.map(function(dataset, i) { + var labelOpts = chart.options.legend.labels; + var style = labelOpts.style; + var type = chart.getDatasetMeta(i).type; + + if (!style) { + // `usePointStyle` is deprecated. To be removed at version 3 + if (labelOpts.usePointStyle) { + style = 'point'; + } else if (type === 'line' || type === 'radar') { + style = 'line'; + } + } + return { text: dataset.label, fillStyle: (!helpers.isArray(dataset.backgroundColor) ? dataset.backgroundColor : dataset.backgroundColor[0]), @@ -58,6 +71,7 @@ defaults._set('global', { lineWidth: dataset.borderWidth, strokeStyle: dataset.borderColor, pointStyle: dataset.pointStyle, + style: style, // Below is extra data used for toggling the datasets datasetIndex: i @@ -83,13 +97,14 @@ defaults._set('global', { }); /** - * Helper function to get the box width based on the usePointStyle option - * @param labelopts {Object} the label options on the legend + * Helper function to get the box width based on the style option + * @param legendItem {Object} the legend item + * @param labelOpts {Object} the label options on the legend * @param fontSize {Number} the label font size * @return {Number} width of the color box area */ -function getBoxWidth(labelOpts, fontSize) { - return labelOpts.usePointStyle ? +function getBoxWidth(legendItem, labelOpts, fontSize) { + return legendItem.style === 'point' ? fontSize * Math.SQRT2 : labelOpts.boxWidth; } @@ -247,7 +262,7 @@ var Legend = Element.extend({ ctx.textBaseline = 'top'; helpers.each(me.legendItems, function(legendItem, i) { - var boxWidth = getBoxWidth(labelOpts, fontSize); + var boxWidth = getBoxWidth(legendItem, labelOpts, fontSize); var width = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; if (lineWidths[lineWidths.length - 1] + width + labelOpts.padding >= me.width) { @@ -277,7 +292,7 @@ var Legend = Element.extend({ var itemHeight = fontSize + vPadding; helpers.each(me.legendItems, function(legendItem, i) { - var boxWidth = getBoxWidth(labelOpts, fontSize); + var boxWidth = getBoxWidth(legendItem, labelOpts, fontSize); var itemWidth = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; // If too tall, go to new column @@ -346,11 +361,10 @@ var Legend = Element.extend({ ctx.fillStyle = fontColor; // render in correct colour ctx.font = labelFont; - var boxWidth = getBoxWidth(labelOpts, fontSize); var hitboxes = me.legendHitBoxes; // current position - var drawLegendBox = function(x, y, legendItem) { + var drawLegendBox = function(x, y, legendItem, boxWidth) { if (isNaN(boxWidth) || boxWidth <= 0) { return; } @@ -371,7 +385,7 @@ var Legend = Element.extend({ ctx.setLineDash(valueOrDefault(legendItem.lineDash, lineDefault.borderDash)); } - if (opts.labels && opts.labels.usePointStyle) { + if (legendItem.style === 'point') { // Recalculate x and y for drawPoint() because its expecting // x and y to be center of figure (instead of top left) var radius = fontSize * Math.SQRT2 / 2; @@ -381,6 +395,14 @@ var Legend = Element.extend({ // Draw pointStyle as legend symbol helpers.canvas.drawPoint(ctx, legendItem.pointStyle, radius, centerX, centerY); + } else if (legendItem.style === 'line') { + // Draw line as legend symbol + if (!isLineWidthZero) { + ctx.beginPath(); + ctx.moveTo(x, y + fontSize / 2); + ctx.lineTo(x + boxWidth, y + fontSize / 2); + ctx.stroke(); + } } else { // Draw box as legend symbol if (!isLineWidthZero) { @@ -391,7 +413,7 @@ var Legend = Element.extend({ ctx.restore(); }; - var fillText = function(x, y, legendItem, textWidth) { + var fillText = function(x, y, legendItem, boxWidth, textWidth) { var halfFontSize = fontSize / 2; var xLeft = boxWidth + halfFontSize + x; var yMiddle = y + halfFontSize; @@ -426,6 +448,7 @@ var Legend = Element.extend({ var itemHeight = fontSize + labelOpts.padding; helpers.each(me.legendItems, function(legendItem, i) { + var boxWidth = getBoxWidth(legendItem, labelOpts, fontSize); var textWidth = ctx.measureText(legendItem.text).width; var width = boxWidth + (fontSize / 2) + textWidth; var x = cursor.x; @@ -443,13 +466,13 @@ var Legend = Element.extend({ cursor.line++; } - drawLegendBox(x, y, legendItem); + drawLegendBox(x, y, legendItem, boxWidth); hitboxes[i].left = x; hitboxes[i].top = y; // Fill the actual label - fillText(x, y, legendItem, textWidth); + fillText(x, y, legendItem, boxWidth, textWidth); if (isHorizontal) { cursor.x += width + (labelOpts.padding); diff --git a/test/specs/global.defaults.tests.js b/test/specs/global.defaults.tests.js index a01284c1a0b..512764326e9 100644 --- a/test/specs/global.defaults.tests.js +++ b/test/specs/global.defaults.tests.js @@ -128,21 +128,24 @@ describe('Default Configs', function() { hidden: false, index: 0, strokeStyle: '#000', - lineWidth: 2 + lineWidth: 2, + style: undefined }, { text: 'label2', fillStyle: 'green', hidden: false, index: 1, strokeStyle: '#000', - lineWidth: 2 + lineWidth: 2, + style: undefined }, { text: 'label3', fillStyle: 'blue', hidden: true, index: 2, strokeStyle: '#000', - lineWidth: 2 + lineWidth: 2, + style: undefined }]; expect(chart.legend.legendItems).toEqual(expected); }); @@ -244,21 +247,24 @@ describe('Default Configs', function() { hidden: false, index: 0, strokeStyle: '#000', - lineWidth: 2 + lineWidth: 2, + style: undefined }, { text: 'label2', fillStyle: 'green', hidden: false, index: 1, strokeStyle: '#000', - lineWidth: 2 + lineWidth: 2, + style: undefined }, { text: 'label3', fillStyle: 'blue', hidden: true, index: 2, strokeStyle: '#000', - lineWidth: 2 + lineWidth: 2, + style: undefined }]; expect(chart.legend.legendItems).toEqual(expected); }); diff --git a/test/specs/global.deprecations.tests.js b/test/specs/global.deprecations.tests.js index 535b9af3307..d94e934609f 100644 --- a/test/specs/global.deprecations.tests.js +++ b/test/specs/global.deprecations.tests.js @@ -6,6 +6,32 @@ describe('Deprecations', function() { expect(Chart.layoutService).toBe(Chart.layouts); }); }); + + describe('Legend Labels: usePointStyle option', function() { + it('should use the style property', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: '', + data: [] + }], + labels: [] + }, + options: { + legend: { + labels: { + usePointStyle: true + } + } + } + }); + + expect(chart.legend.legendItems[0].style).toEqual('point'); + expect(chart.legend.legendHitBoxes[0].height).toBeCloseToPixel(12); + expect(chart.legend.legendHitBoxes[0].width).toBeCloseToPixel(23); + }); + }); }); describe('Version 2.7.0', function() { diff --git a/test/specs/plugin.legend.tests.js b/test/specs/plugin.legend.tests.js index 5b75069aaea..3e3bdbdb573 100644 --- a/test/specs/plugin.legend.tests.js +++ b/test/specs/plugin.legend.tests.js @@ -58,6 +58,7 @@ describe('Legend block tests', function() { lineWidth: undefined, strokeStyle: undefined, pointStyle: undefined, + style: undefined, datasetIndex: 0 }, { text: 'dataset2', @@ -70,6 +71,7 @@ describe('Legend block tests', function() { lineWidth: undefined, strokeStyle: undefined, pointStyle: undefined, + style: undefined, datasetIndex: 1 }, { text: 'dataset3', @@ -82,6 +84,7 @@ describe('Legend block tests', function() { lineWidth: 10, strokeStyle: 'green', pointStyle: 'crossRot', + style: undefined, datasetIndex: 2 }]); }); @@ -135,6 +138,7 @@ describe('Legend block tests', function() { lineWidth: undefined, strokeStyle: undefined, pointStyle: undefined, + style: undefined, datasetIndex: 0 }, { text: 'dataset3', @@ -147,10 +151,124 @@ describe('Legend block tests', function() { lineWidth: 10, strokeStyle: 'green', pointStyle: 'crossRot', + style: undefined, datasetIndex: 2 }]); }); + it('should set the label styles correctly when the dataset types are mixed', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + type: 'line', + label: 'dataset1', + backgroundColor: '#f31', + borderCapStyle: 'butt', + borderDash: [2, 2], + borderDashOffset: 5.5, + data: [] + }, { + type: 'bar', + label: 'dataset2', + hidden: true, + borderJoinStyle: 'miter', + data: [], + legendHidden: true + }], + labels: [] + } + }); + + expect(chart.legend.legendItems).toEqual([{ + text: 'dataset1', + fillStyle: '#f31', + hidden: false, + lineCap: 'butt', + lineDash: [2, 2], + lineDashOffset: 5.5, + lineJoin: undefined, + lineWidth: undefined, + strokeStyle: undefined, + pointStyle: undefined, + style: 'line', + datasetIndex: 0 + }, { + text: 'dataset2', + fillStyle: undefined, + hidden: true, + lineCap: undefined, + lineDash: undefined, + lineDashOffset: undefined, + lineJoin: 'miter', + lineWidth: undefined, + strokeStyle: undefined, + pointStyle: undefined, + style: undefined, + datasetIndex: 1 + }]); + }); + + it('should set the label style to point when the style option is set to point', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + type: 'line', + label: 'dataset1', + backgroundColor: '#f31', + borderCapStyle: 'butt', + borderDash: [2, 2], + borderDashOffset: 5.5, + data: [] + }, { + type: 'bar', + label: 'dataset2', + hidden: true, + borderJoinStyle: 'miter', + data: [], + legendHidden: true + }], + labels: [] + }, + options: { + legend: { + labels: { + style: 'point' + }, + } + } + }); + + expect(chart.legend.legendItems).toEqual([{ + text: 'dataset1', + fillStyle: '#f31', + hidden: false, + lineCap: 'butt', + lineDash: [2, 2], + lineDashOffset: 5.5, + lineJoin: undefined, + lineWidth: undefined, + strokeStyle: undefined, + pointStyle: undefined, + style: 'point', + datasetIndex: 0 + }, { + text: 'dataset2', + fillStyle: undefined, + hidden: true, + lineCap: undefined, + lineDash: undefined, + lineDashOffset: undefined, + lineJoin: 'miter', + lineWidth: undefined, + strokeStyle: undefined, + pointStyle: undefined, + style: 'point', + datasetIndex: 1 + }]); + }); + it('should not throw when the label options are missing', function() { var makeChart = function() { window.acquireChart({ @@ -176,7 +294,7 @@ describe('Legend block tests', function() { expect(makeChart).not.toThrow(); }); - it('should draw correctly', function() { + it('should draw correctly when the style option is set to point', function() { var chart = window.acquireChart({ type: 'bar', data: { @@ -389,6 +507,53 @@ describe('Legend block tests', function() { }]);*/ }); + it('should draw correctly when ', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + label: 'dataset1', + backgroundColor: '#f31', + borderCapStyle: 'butt', + borderDash: [2, 2], + borderDashOffset: 5.5, + data: [] + }, { + label: 'dataset2', + hidden: true, + borderJoinStyle: 'miter', + data: [] + }, { + label: 'dataset3', + borderWidth: 10, + borderColor: 'green', + data: [] + }], + labels: [] + }, + options: { + legend: { + labels: { + style: 'point' + }, + } + } + }); + + expect(chart.legend.legendHitBoxes.length).toBe(3); + + [ + {h: 12, l: 137, t: 10, w: 70}, + {h: 12, l: 216, t: 10, w: 70}, + {h: 12, l: 295, t: 10, w: 70} + ].forEach(function(expected, i) { + expect(chart.legend.legendHitBoxes[i].height).toBeCloseToPixel(expected.h); + expect(chart.legend.legendHitBoxes[i].left).toBeCloseToPixel(expected.l); + expect(chart.legend.legendHitBoxes[i].top).toBeCloseToPixel(expected.t); + expect(chart.legend.legendHitBoxes[i].width).toBeCloseToPixel(expected.w); + }); + }); + describe('config update', function() { it ('should update the options', function() { var chart = acquireChart({