diff --git a/docs/charts/bar.md b/docs/charts/bar.md index e4d146b2c67..31b2385789c 100644 --- a/docs/charts/bar.md +++ b/docs/charts/bar.md @@ -77,6 +77,7 @@ Some properties can be specified as an array. If these are set to an array value | `hoverBackgroundColor` | `Color/Color[]` | The fill colour of the bars when hovered. | `hoverBorderColor` | `Color/Color[]` | The stroke colour of the bars when hovered. | `hoverBorderWidth` | `Number/Number[]` | The stroke width of the bars when hovered. +| `legend` | `object` | Style of the legend if point style is not used. [more...](../configuration/legend.md/#dataset-legend-configuration) ### borderSkipped This setting is used to avoid drawing the bar stroke at the base of the fill. In general, this does not need to be changed except when creating chart types that derive from a bar chart. diff --git a/docs/charts/bubble.md b/docs/charts/bubble.md index e9f8a7216b1..8d138df32a6 100644 --- a/docs/charts/bubble.md +++ b/docs/charts/bubble.md @@ -53,6 +53,7 @@ The bubble chart allows a number of properties to be specified for each dataset. | [`pointStyle`](#styling) | `String` | Yes | Yes | `circle` | [`rotation`](#styling) | `Number` | Yes | Yes | `0` | [`radius`](#styling) | `Number` | Yes | Yes | `3` +| [`legend`](../configuration/legend.md/#dataset-legend-configuration) | `object` | - | - | ### Labeling diff --git a/docs/charts/doughnut.md b/docs/charts/doughnut.md index cd9af9d56a0..74c23f00e6b 100644 --- a/docs/charts/doughnut.md +++ b/docs/charts/doughnut.md @@ -61,6 +61,7 @@ The doughnut/pie chart allows a number of properties to be specified for each da | `hoverBackgroundColor` | `Color[]` | The fill colour of the arcs when hovered. | `hoverBorderColor` | `Color[]` | The stroke colour of the arcs when hovered. | `hoverBorderWidth` | `Number[]` | The stroke width of the arcs when hovered. +| `legend` | `object` | Style of the legend if point style is not used. [more...](../configuration/legend.md/#dataset-legend-configuration) ## Config Options diff --git a/docs/charts/line.md b/docs/charts/line.md index db0245b1e0c..d1e8e66f6ee 100644 --- a/docs/charts/line.md +++ b/docs/charts/line.md @@ -72,6 +72,7 @@ All point* properties can be specified as an array. If these are set to an array | `showLine` | `Boolean` | If false, the line is not drawn for this dataset. | `spanGaps` | `Boolean` | If true, lines will be drawn between points with no or null data. If false, points with `NaN` data will create a break in the line | `steppedLine` | `Boolean/String` | If the line is shown as a stepped line. [more...](#stepped-line) +| `legend` | `object` | Style of the legend if point style is not used. [more...](../configuration/legend.md/#dataset-legend-configuration) ### cubicInterpolationMode The following interpolation modes are supported: diff --git a/docs/charts/polar.md b/docs/charts/polar.md index 8f403149e05..efb4ce55bc5 100644 --- a/docs/charts/polar.md +++ b/docs/charts/polar.md @@ -52,6 +52,7 @@ The following options can be included in a polar area chart dataset to configure | `hoverBackgroundColor` | `Color[]` | The fill colour of the arcs when hovered. | `hoverBorderColor` | `Color[]` | The stroke colour of the arcs when hovered. | `hoverBorderWidth` | `Number[]` | The stroke width of the arcs when hovered. +| `legend` | `object` | Style of the legend if point style is not used. [more...](../configuration/legend.md/#dataset-legend-configuration) ## Config Options diff --git a/docs/charts/radar.md b/docs/charts/radar.md index 947e15a3102..4dc5fdbf1a0 100644 --- a/docs/charts/radar.md +++ b/docs/charts/radar.md @@ -89,6 +89,7 @@ All point* properties can be specified as an array. If these are set to an array | `pointHoverBorderColor` | `Color/Color[]` | Point border color when hovered. | `pointHoverBorderWidth` | `Number/Number[]` | Border width of point when hovered. | `pointHoverRadius` | `Number/Number[]` | The radius of the point when hovered. +| `legend` | `object` | Style of the legend if point style is not used. [more...](../configuration/legend.md/#dataset-legend-configuration) ### pointStyle The style of point. Options are: diff --git a/docs/configuration/legend.md b/docs/configuration/legend.md index d2a3a88b3b6..9729ce0001b 100644 --- a/docs/configuration/legend.md +++ b/docs/configuration/legend.md @@ -28,7 +28,7 @@ The legend label configuration is nested below the legend configuration using th | Name | Type | Default | Description | -----| ---- | --------| ----------- -| `boxWidth` | `Number` | `40` | width of coloured box +| `boxWidth` | `Number` | `40` | width of symbol if point style not used | `fontSize` | `Number` | `12` | font size of text | `fontStyle` | `String` | `'normal'` | font style of text | `fontColor` | `Color` | `'#666'` | Color of text @@ -37,6 +37,7 @@ The legend label configuration is nested below the legend configuration using th | `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). +| `symbol` | `String` | `rect` | Legend symbol to display if point style is not used. Available values from [PointStyle](./elements.md/#point-styles) ## Legend Item Interface @@ -73,6 +74,12 @@ Items passed to the legend `onClick` function are the ones returned from `labels // Point style of the legend box (only used if usePointStyle is true) pointStyle: String + + // Legend symbol to display if point style is not used. + symbol: String + + // Width of legend symbol if point style is not used. + boxWidth: Number } ``` @@ -164,7 +171,19 @@ var chart = new Chart(ctx, { } }); ``` - Note that legendCallback is not called automatically and you must call `generateLegend()` yourself in code when creating a legend using this method. +### Dataset Legend Configuration + +The following legend properties can be defined at dataset level. Array is accepted for +all those properties for doughnut and polar chart types. + +| Name | Type | Description +| -----| ---- | ----------- +| `symbol` | `String` | Legend symbol to display if point style is not used. Available values from [PointStyle](./elements.md/#point-styles). +| `boxWidth` | `Number` | width of symbol if point style not used +| `borderWidth` | `Number` | width of symbol border or symbol line +| `borderColor` | `Number` | color of symbol border or symbol line + + diff --git a/src/controllers/controller.doughnut.js b/src/controllers/controller.doughnut.js index eb759fe60b8..8d15c495ff1 100644 --- a/src/controllers/controller.doughnut.js +++ b/src/controllers/controller.doughnut.js @@ -39,6 +39,7 @@ defaults._set('doughnut', { labels: { generateLabels: function(chart) { var data = chart.data; + var opts = chart.options.legend.labels; if (data.labels.length && data.datasets.length) { return data.labels.map(function(label, i) { var meta = chart.getDatasetMeta(0); @@ -50,14 +51,14 @@ 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); - return { text: label, fillStyle: fill, - strokeStyle: stroke, - lineWidth: bw, + strokeStyle: helpers.valueOrDefault((ds.legend && ds.legend.borderColor && ds.legend.borderColor[i]), stroke), + lineWidth: helpers.valueOrDefault((ds.legend && ds.legend.borderWidth && ds.legend.borderWidth[i]), bw), hidden: isNaN(ds.data[i]) || meta.data[i].hidden, - + legendSymbol: helpers.valueOrDefault((ds.legend && ds.legend.symbol && ds.legend.symbol[i]), opts.symbol), + boxWidth: helpers.valueOrDefault((ds.legend && ds.legend.boxWidth && ds.legend.boxWidth[i]), opts.boxWidth), // 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..dd84760ac5b 100644 --- a/src/controllers/controller.polarArea.js +++ b/src/controllers/controller.polarArea.js @@ -53,6 +53,7 @@ defaults._set('polarArea', { labels: { generateLabels: function(chart) { var data = chart.data; + var opts = chart.options.legend.labels; if (data.labels.length && data.datasets.length) { return data.labels.map(function(label, i) { var meta = chart.getDatasetMeta(0); @@ -64,14 +65,14 @@ 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); - return { text: label, fillStyle: fill, - strokeStyle: stroke, - lineWidth: bw, + strokeStyle: helpers.valueOrDefault((ds.legend && ds.legend.borderColor && ds.legend.borderColor[i]), stroke), + lineWidth: helpers.valueOrDefault((ds.legend && ds.legend.borderWidth && ds.legend.borderWidth[i]), bw), hidden: isNaN(ds.data[i]) || meta.data[i].hidden, - + legendSymbol: helpers.valueOrDefault((ds.legend && ds.legend.symbol && ds.legend.symbol[i]), opts.symbol), + boxWidth: helpers.valueOrDefault((ds.legend && ds.legend.boxWidth && ds.legend.boxWidth[i]), opts.boxWidth), // Extra data used for toggling the correct item index: i }; diff --git a/src/helpers/helpers.canvas.js b/src/helpers/helpers.canvas.js index 60fb6e1a299..ccc1b53eff4 100644 --- a/src/helpers/helpers.canvas.js +++ b/src/helpers/helpers.canvas.js @@ -49,103 +49,119 @@ var exports = module.exports = { }, drawPoint: function(ctx, style, radius, x, y, rotation) { - var type, edgeLength, xOffset, yOffset, height, size; - rotation = rotation || 0; + // call drawSymbol with converted radius to width and height + // and move x, y to the top left corner + this.drawSymbol(ctx, style, radius * 2, radius * 2, x - radius, y - radius, rotation, true); + }, + + drawSymbol: function(ctx, style, width, height, x, y, rotation, isPoint) { if (style && typeof style === 'object') { - type = style.toString(); + var type = style.toString(); if (type === '[object HTMLImageElement]' || type === '[object HTMLCanvasElement]') { - ctx.drawImage(style, x - style.width / 2, y - style.height / 2, style.width, style.height); - return; + ctx.drawImage(style, x, y, style.width, style.height); + return false; } } - - if (isNaN(radius) || radius <= 0) { - return; + if (isNaN(width) || width <= 0) { + return false; + } + var vx, vy; + if (rotation) { + ctx.save(); + ctx.translate(x + width / 2, y + width / 2); + ctx.rotate(rotation * Math.PI / 180); + ctx.translate(-width / 2, -width / 2); + vx = 0; + vy = 0; + } else { + vx = x; + vy = y; } + var radius = width / 2; + var yRadius = height / 2; + + // some symbols are not using the full width when they're called as a point + // e.g. rect, rectRounded, rectRot, crossRot and star + // Following variables are used to define the pading value for those symbols + // the full width and height is used when symbol is called as legend. + var padLeft = isPoint ? radius - width / 2 / Math.sqrt(2) : 0; + var padRight = isPoint ? radius + width / 2 / Math.sqrt(2) : width; + var padTop = isPoint ? radius - height / 2 / Math.sqrt(2) : 0; + var padBottom = isPoint ? radius + height / 2 / Math.sqrt(2) : height; - ctx.save(); - ctx.translate(x, y); - ctx.rotate(rotation * Math.PI / 180); ctx.beginPath(); switch (style) { - // Default includes circle + // Default circle default: - ctx.arc(0, 0, radius, 0, Math.PI * 2); + // display standard circle if height and width are the same otherwise display a RectRounded + if (width === height) { + ctx.arc(vx + radius, vy + radius, radius, 0, Math.PI * 2); + } else { + this.roundedRect(ctx, vx + padLeft, vy + padTop, -padLeft + padRight, -padTop + padBottom, radius * 0.425); + } ctx.closePath(); break; - case 'triangle': - edgeLength = 3 * radius / Math.sqrt(3); - height = edgeLength * Math.sqrt(3) / 2; - ctx.moveTo(-edgeLength / 2, height / 3); - ctx.lineTo(edgeLength / 2, height / 3); - ctx.lineTo(0, -2 * height / 3); + case 'rect': + ctx.rect(vx + padLeft, vy + padTop, -padLeft + padRight, -padTop + padBottom); ctx.closePath(); break; - case 'rect': - size = 1 / Math.SQRT2 * radius; - ctx.rect(-size, -size, 2 * size, 2 * size); + case 'triangle': + ctx.moveTo(vx + radius, vy); + ctx.lineTo(vx + radius - (radius * Math.sqrt(3) / 2), vy + height * 0.75); + ctx.lineTo(vx + radius + (radius * Math.sqrt(3) / 2), vy + height * 0.75); + ctx.closePath(); break; case 'rectRounded': - var offset = radius / Math.SQRT2; - var leftX = -offset; - var topY = -offset; - var sideSize = Math.SQRT2 * radius; - - // NOTE(SB) the rounded rect implementation changed to use `arcTo` - // instead of `quadraticCurveTo` since it generates better results - // when rect is almost a circle. 0.425 (instead of 0.5) produces - // results visually closer to the previous impl. - this.roundedRect(ctx, leftX, topY, sideSize, sideSize, radius * 0.425); + this.roundedRect(ctx, vx + padLeft, vy + padTop, -padLeft + padRight, -padTop + padBottom, radius * 0.425); + ctx.closePath(); break; case 'rectRot': - size = 1 / Math.SQRT2 * radius; - ctx.moveTo(-size, 0); - ctx.lineTo(0, size); - ctx.lineTo(size, 0); - ctx.lineTo(0, -size); + ctx.moveTo(vx + padLeft, vy + yRadius); + ctx.lineTo(vx + radius, vy + padTop); + ctx.lineTo(vx + padRight, vy + yRadius); + ctx.lineTo(vx + radius, vy + padBottom); ctx.closePath(); break; case 'cross': - ctx.moveTo(0, radius); - ctx.lineTo(0, -radius); - ctx.moveTo(-radius, 0); - ctx.lineTo(radius, 0); + ctx.moveTo(vx + radius, vy); + ctx.lineTo(vx + radius, vy + height); + ctx.moveTo(vx, vy + yRadius); + ctx.lineTo(vx + width, vy + yRadius); break; case 'crossRot': - xOffset = Math.cos(Math.PI / 4) * radius; - yOffset = Math.sin(Math.PI / 4) * radius; - ctx.moveTo(-xOffset, -yOffset); - ctx.lineTo(xOffset, yOffset); - ctx.moveTo(-xOffset, yOffset); - ctx.lineTo(xOffset, -yOffset); + ctx.moveTo(vx + padLeft, vy + padTop); + ctx.lineTo(vx + padRight, vy + padBottom); + ctx.moveTo(vx + padLeft, vy + padBottom); + ctx.lineTo(vx + padRight, vy + padTop); break; case 'star': - ctx.moveTo(0, radius); - ctx.lineTo(0, -radius); - ctx.moveTo(-radius, 0); - ctx.lineTo(radius, 0); - xOffset = Math.cos(Math.PI / 4) * radius; - yOffset = Math.sin(Math.PI / 4) * radius; - ctx.moveTo(-xOffset, -yOffset); - ctx.lineTo(xOffset, yOffset); - ctx.moveTo(-xOffset, yOffset); - ctx.lineTo(xOffset, -yOffset); + ctx.moveTo(vx + radius, vy); + ctx.lineTo(vx + radius, vy + height); + ctx.moveTo(vx, vy + yRadius); + ctx.lineTo(vx + width, vy + yRadius); + ctx.moveTo(vx + padLeft, vy + padTop); + ctx.lineTo(vx + padRight, vy + padBottom); + ctx.moveTo(vx + padLeft, vy + padBottom); + ctx.lineTo(vx + padRight, vy + padTop); break; case 'line': - ctx.moveTo(-radius, 0); - ctx.lineTo(radius, 0); + ctx.moveTo(vx, vy + yRadius); + ctx.lineTo(vx + width, vy + yRadius); break; case 'dash': - ctx.moveTo(0, 0); - ctx.lineTo(radius, 0); + ctx.moveTo(vx + radius, vy + yRadius); + ctx.lineTo(vx + width, vy + yRadius); break; } - ctx.fill(); ctx.stroke(); - ctx.restore(); + if (rotation) { + ctx.restore(); + } + return true; + }, clipArea: function(ctx, area) { diff --git a/src/plugins/plugin.legend.js b/src/plugins/plugin.legend.js index 3f1559c3003..1096e722710 100644 --- a/src/plugins/plugin.legend.js +++ b/src/plugins/plugin.legend.js @@ -31,6 +31,7 @@ defaults._set('global', { onHover: null, labels: { + symbol: 'rect', boxWidth: 40, padding: 10, // Generates labels shown in the legend @@ -44,8 +45,13 @@ defaults._set('global', { // lineDashOffset : // lineJoin : // lineWidth : + // pointStyle: symbol use on legend when usePointStyle is set + // legendSymbol : Symbol use on legend if not usePointStyle + // boxWidth : width of legend symbol if not userPointStyle generateLabels: function(chart) { var data = chart.data; + var opts = chart.options.legend.labels; + var pointSize = helpers.valueOrDefault(opts.fontSize, defaults.global.defaultFontSize); return helpers.isArray(data.datasets) ? data.datasets.map(function(dataset, i) { return { text: dataset.label, @@ -55,10 +61,11 @@ defaults._set('global', { lineDash: dataset.borderDash, lineDashOffset: dataset.borderDashOffset, lineJoin: dataset.borderJoinStyle, - lineWidth: dataset.borderWidth, - strokeStyle: dataset.borderColor, + lineWidth: helpers.valueOrDefault((dataset.legend && dataset.legend.borderWidth), dataset.borderWidth), + strokeStyle: helpers.valueOrDefault((dataset.legend && dataset.legend.borderColor), dataset.borderColor), pointStyle: dataset.pointStyle, - + legendSymbol: helpers.valueOrDefault((dataset.legend && dataset.legend.symbol), opts.symbol), + boxWidth: opts.usePointStyle ? pointSize : helpers.valueOrDefault((dataset.legend && dataset.legend.boxWidth), opts.boxWidth), // Below is extra data used for toggling the datasets datasetIndex: i }; @@ -82,18 +89,6 @@ defaults._set('global', { } }); -/** - * Helper function to get the box width based on the usePointStyle option - * @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 ? - fontSize * Math.SQRT2 : - labelOpts.boxWidth; -} - /** * IMPORTANT: this class is exposed publicly as Chart.Legend, backward compatibility required! */ @@ -247,8 +242,7 @@ var Legend = Element.extend({ ctx.textBaseline = 'top'; helpers.each(me.legendItems, function(legendItem, i) { - var boxWidth = getBoxWidth(labelOpts, fontSize); - var width = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; + var width = legendItem.boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; if (lineWidths[lineWidths.length - 1] + width + labelOpts.padding >= me.width) { totalHeight += fontSize + (labelOpts.padding); @@ -277,8 +271,7 @@ var Legend = Element.extend({ var itemHeight = fontSize + vPadding; helpers.each(me.legendItems, function(legendItem, i) { - var boxWidth = getBoxWidth(labelOpts, fontSize); - var itemWidth = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; + var itemWidth = legendItem.boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; // If too tall, go to new column if (currentColHeight + itemHeight > minSize.height) { @@ -346,12 +339,11 @@ 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) { - if (isNaN(boxWidth) || boxWidth <= 0) { + if (isNaN(legendItem.boxWidth) || legendItem.boxWidth <= 0) { return; } @@ -371,29 +363,16 @@ var Legend = Element.extend({ ctx.setLineDash(valueOrDefault(legendItem.lineDash, lineDefault.borderDash)); } - if (opts.labels && opts.labels.usePointStyle) { - // 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; - var offSet = radius / Math.SQRT2; - var centerX = x + offSet; - var centerY = y + offSet; - - // Draw pointStyle as legend symbol - helpers.canvas.drawPoint(ctx, legendItem.pointStyle, radius, centerX, centerY); - } else { - // Draw box as legend symbol - if (!isLineWidthZero) { - ctx.strokeRect(x, y, boxWidth, fontSize); - } - ctx.fillRect(x, y, boxWidth, fontSize); + var strokeMe = helpers.canvas.drawSymbol(ctx, (labelOpts.usePointStyle ? legendItem.pointStyle : legendItem.legendSymbol), legendItem.boxWidth, fontSize, x, y); + if (!isLineWidthZero && strokeMe) { + ctx.stroke(); } ctx.restore(); }; var fillText = function(x, y, legendItem, textWidth) { var halfFontSize = fontSize / 2; - var xLeft = boxWidth + halfFontSize + x; + var xLeft = legendItem.boxWidth + halfFontSize + x; var yMiddle = y + halfFontSize; ctx.fillText(legendItem.text, xLeft, yMiddle); @@ -427,7 +406,7 @@ var Legend = Element.extend({ var itemHeight = fontSize + labelOpts.padding; helpers.each(me.legendItems, function(legendItem, i) { var textWidth = ctx.measureText(legendItem.text).width; - var width = boxWidth + (fontSize / 2) + textWidth; + var width = legendItem.boxWidth + (fontSize / 2) + textWidth; var x = cursor.x; var y = cursor.y; diff --git a/test/specs/element.point.tests.js b/test/specs/element.point.tests.js index 8d1227003b9..80f9155f6a7 100644 --- a/test/specs/element.point.tests.js +++ b/test/specs/element.point.tests.js @@ -94,7 +94,7 @@ describe('Chart.elements.Point', function() { expect(point.getCenterPoint()).toEqual({x: 10, y: 10}); }); - it ('should draw correctly with default settings if necessary', function() { + it ('should draw correctly', function() { var mockContext = window.createMockContext(); var point = new Chart.elements.Point({ _datasetIndex: 2, @@ -107,41 +107,277 @@ describe('Chart.elements.Point', function() { // Attach a view object as if we were the controller point._view = { radius: 2, + pointStyle: 'circle', + rotation: 25, hitRadius: 3, + borderColor: 'rgba(1, 2, 3, 1)', + borderWidth: 6, + backgroundColor: 'rgba(0, 255, 0)', x: 10, y: 15, ctx: mockContext }; + var tx = point._view.x - point._view.radius; + var ty = point._view.y - point._view.radius; + var tw = point._view.radius * 2; + var zx = point._view.radius / Math.sqrt(2); point.draw(); expect(mockContext.getCalls()).toEqual([{ name: 'setStrokeStyle', - args: ['rgba(0,0,0,0.1)'] + args: ['rgba(1, 2, 3, 1)'] }, { name: 'setLineWidth', - args: [1] + args: [6] }, { name: 'setFillStyle', - args: ['rgba(0,0,0,0.1)'] + args: ['rgba(0, 255, 0)'] }, { name: 'save', args: [] }, { name: 'translate', - args: [10, 15] + args: [tx + tw / 2, ty + tw / 2] }, { name: 'rotate', - args: [0] + args: [25 * Math.PI / 180] + }, { + name: 'translate', + args: [-tw / 2, -tw / 2] }, { name: 'beginPath', args: [] }, { name: 'arc', - args: [0, 0, 2, 0, 2 * Math.PI] + args: [2, 2, 2, 0, 2 * Math.PI] + }, { + name: 'closePath', + args: [], + }, { + name: 'fill', + args: [], + }, { + name: 'stroke', + args: [] + }, { + name: 'restore', + args: [] + }]); + + mockContext.resetCalls(); + point._view.pointStyle = 'triangle'; + point.draw(); + + expect(mockContext.getCalls()).toEqual([{ + name: 'setStrokeStyle', + args: ['rgba(1, 2, 3, 1)'] + }, { + name: 'setLineWidth', + args: [6] + }, { + name: 'setFillStyle', + args: ['rgba(0, 255, 0)'] + }, { + name: 'save', + args: [] + }, { + name: 'translate', + args: [tx + tw / 2, ty + tw / 2] + }, { + name: 'rotate', + args: [25 * Math.PI / 180] + }, { + name: 'translate', + args: [-tw / 2, -tw / 2] + }, { + name: 'beginPath', + args: [] + }, { + name: 'moveTo', + args: [tw / 2, 0] + }, { + name: 'lineTo', + args: [tw / 2 - tw * Math.sqrt(3) / 4, tw * 0.75], + }, { + name: 'lineTo', + args: [tw / 2 + tw * Math.sqrt(3) / 4, tw * 0.75], + }, { + name: 'closePath', + args: [], + }, { + name: 'fill', + args: [], + }, { + name: 'stroke', + args: [] + }, { + name: 'restore', + args: [] + }]); + + mockContext.resetCalls(); + point._view.pointStyle = 'rect'; + point.draw(); + + expect(mockContext.getCalls()).toEqual([{ + name: 'setStrokeStyle', + args: ['rgba(1, 2, 3, 1)'] + }, { + name: 'setLineWidth', + args: [6] + }, { + name: 'setFillStyle', + args: ['rgba(0, 255, 0)'] + }, { + name: 'save', + args: [] + }, { + name: 'translate', + args: [tx + tw / 2, ty + tw / 2] + }, { + name: 'rotate', + args: [25 * Math.PI / 180] + }, { + name: 'translate', + args: [-tw / 2, -tw / 2] + }, { + name: 'beginPath', + args: [] + }, { + name: 'rect', + args: [tw / 2 - zx, tw / 2 - zx, zx * 2, zx * 2] + }, { + name: 'closePath', + args: [], + }, { + name: 'fill', + args: [], + }, { + name: 'stroke', + args: [] + }, { + name: 'restore', + args: [] + }]); + + var drawRoundedRectangleSpy = jasmine.createSpy('drawRoundedRectangle'); + var drawRoundedRectangle = Chart.helpers.canvas.roundedRect; + Chart.helpers.canvas.roundedRect = drawRoundedRectangleSpy; + mockContext.resetCalls(); + point._view.pointStyle = 'rectRounded'; + point.draw(); + + expect(drawRoundedRectangleSpy).toHaveBeenCalledWith( + mockContext, + tw / 2 - zx, + tw / 2 - zx, + zx * 2, + zx * 2, + tw * 0.2125 + ); + expect(mockContext.getCalls()).toContain( + jasmine.objectContaining({ + name: 'fill', + args: [], + }) + ); + + Chart.helpers.canvas.roundedRect = drawRoundedRectangle; + mockContext.resetCalls(); + point._view.pointStyle = 'rectRot'; + point.draw(); + + expect(mockContext.getCalls()).toEqual([{ + name: 'setStrokeStyle', + args: ['rgba(1, 2, 3, 1)'] + }, { + name: 'setLineWidth', + args: [6] + }, { + name: 'setFillStyle', + args: ['rgba(0, 255, 0)'] + }, { + name: 'save', + args: [] + }, { + name: 'translate', + args: [tx + tw / 2, ty + tw / 2] + }, { + name: 'rotate', + args: [25 * Math.PI / 180] + }, { + name: 'translate', + args: [-tw / 2, -tw / 2] + }, { + name: 'beginPath', + args: [] + }, { + name: 'moveTo', + args: [tw / 2 - zx, tw / 2] + }, { + name: 'lineTo', + args: [tw / 2, tw / 2 - zx] + }, { + name: 'lineTo', + args: [tw / 2 + zx, tw / 2], + }, { + name: 'lineTo', + args: [tw / 2, tw / 2 + zx], }, { name: 'closePath', + args: [] + }, { + name: 'fill', args: [], + }, { + name: 'stroke', + args: [] + }, { + name: 'restore', + args: [] + }]); + + mockContext.resetCalls(); + point._view.pointStyle = 'cross'; + point.draw(); + + expect(mockContext.getCalls()).toEqual([{ + name: 'setStrokeStyle', + args: ['rgba(1, 2, 3, 1)'] + }, { + name: 'setLineWidth', + args: [6] + }, { + name: 'setFillStyle', + args: ['rgba(0, 255, 0)'] + }, { + name: 'save', + args: [] + }, { + name: 'translate', + args: [tx + tw / 2, ty + tw / 2] + }, { + name: 'rotate', + args: [25 * Math.PI / 180] + }, { + name: 'translate', + args: [-tw / 2, -tw / 2] + }, { + name: 'beginPath', + args: [] + }, { + name: 'moveTo', + args: [tw / 2, 0] + }, { + name: 'lineTo', + args: [tw / 2, tw], + }, { + name: 'moveTo', + args: [0, tw / 2], + }, { + name: 'lineTo', + args: [tw, tw / 2], }, { name: 'fill', args: [], @@ -152,6 +388,259 @@ describe('Chart.elements.Point', function() { name: 'restore', args: [] }]); + + mockContext.resetCalls(); + point._view.pointStyle = 'crossRot'; + point.draw(); + + expect(mockContext.getCalls()).toEqual([{ + name: 'setStrokeStyle', + args: ['rgba(1, 2, 3, 1)'] + }, { + name: 'setLineWidth', + args: [6] + }, { + name: 'setFillStyle', + args: ['rgba(0, 255, 0)'] + }, { + name: 'save', + args: [] + }, { + name: 'translate', + args: [tx + tw / 2, ty + tw / 2] + }, { + name: 'rotate', + args: [25 * Math.PI / 180] + }, { + name: 'translate', + args: [-tw / 2, -tw / 2] + }, { + name: 'beginPath', + args: [] + }, { + name: 'moveTo', + args: [tw / 2 - zx, tw / 2 - zx] + }, { + name: 'lineTo', + args: [tw / 2 + zx, tw / 2 + zx], + }, { + name: 'moveTo', + args: [tw / 2 - zx, tw / 2 + zx], + }, { + name: 'lineTo', + args: [tw / 2 + zx, tw / 2 - zx], + }, { + name: 'fill', + args: [], + }, { + name: 'stroke', + args: [] + }, { + name: 'restore', + args: [] + }]); + + mockContext.resetCalls(); + point._view.pointStyle = 'star'; + point.draw(); + + expect(mockContext.getCalls()).toEqual([{ + name: 'setStrokeStyle', + args: ['rgba(1, 2, 3, 1)'] + }, { + name: 'setLineWidth', + args: [6] + }, { + name: 'setFillStyle', + args: ['rgba(0, 255, 0)'] + }, { + name: 'save', + args: [] + }, { + name: 'translate', + args: [tx + tw / 2, ty + tw / 2] + }, { + name: 'rotate', + args: [25 * Math.PI / 180] + }, { + name: 'translate', + args: [-tw / 2, -tw / 2] + }, { + name: 'beginPath', + args: [] + }, { + name: 'moveTo', + args: [tw / 2, 0] + }, { + name: 'lineTo', + args: [tw / 2, tw], + }, { + name: 'moveTo', + args: [0, tw / 2], + }, { + name: 'lineTo', + args: [tw, tw / 2], + }, { + name: 'moveTo', + args: [tw / 2 - zx, tw / 2 - zx] + }, { + name: 'lineTo', + args: [tw / 2 + zx, tw / 2 + zx], + }, { + name: 'moveTo', + args: [tw / 2 - zx, tw / 2 + zx], + }, { + name: 'lineTo', + args: [tw / 2 + zx, tw / 2 - zx], + }, { + name: 'fill', + args: [], + }, { + name: 'stroke', + args: [] + }, { + name: 'restore', + args: [] + }]); + + mockContext.resetCalls(); + point._view.pointStyle = 'line'; + point.draw(); + + expect(mockContext.getCalls()).toEqual([{ + name: 'setStrokeStyle', + args: ['rgba(1, 2, 3, 1)'] + }, { + name: 'setLineWidth', + args: [6] + }, { + name: 'setFillStyle', + args: ['rgba(0, 255, 0)'] + }, { + name: 'save', + args: [] + }, { + name: 'translate', + args: [tx + tw / 2, ty + tw / 2] + }, { + name: 'rotate', + args: [25 * Math.PI / 180] + }, { + name: 'translate', + args: [-tw / 2, -tw / 2] + }, { + name: 'beginPath', + args: [] + }, { + name: 'moveTo', + args: [0, tw / 2] + }, { + name: 'lineTo', + args: [tw, tw / 2], + }, { + name: 'fill', + args: [], + }, { + name: 'stroke', + args: [] + }, { + name: 'restore', + args: [] + }]); + + mockContext.resetCalls(); + point._view.pointStyle = 'dash'; + point.draw(); + + expect(mockContext.getCalls()).toEqual([{ + name: 'setStrokeStyle', + args: ['rgba(1, 2, 3, 1)'] + }, { + name: 'setLineWidth', + args: [6] + }, { + name: 'setFillStyle', + args: ['rgba(0, 255, 0)'] + }, { + name: 'save', + args: [] + }, { + name: 'translate', + args: [tx + tw / 2, ty + tw / 2] + }, { + name: 'rotate', + args: [25 * Math.PI / 180] + }, { + name: 'translate', + args: [-tw / 2, -tw / 2] + }, { + name: 'beginPath', + args: [] + }, { + name: 'moveTo', + args: [tw / 2, tw / 2] + }, { + name: 'lineTo', + args: [tw, tw / 2], + }, { + name: 'fill', + args: [], + }, { + name: 'stroke', + args: [] + }, { + name: 'restore', + args: [] + }]); + + }); + + it ('should draw correctly with default settings if necessary', function() { + var mockContext = window.createMockContext(); + var point = new Chart.elements.Point({ + _datasetIndex: 2, + _index: 1, + _chart: { + ctx: mockContext, + } + }); + + // Attach a view object as if we were the controller + point._view = { + radius: 2, + hitRadius: 3, + x: 10, + y: 15, + ctx: mockContext + }; + + point.draw(); + + expect(mockContext.getCalls()).toEqual([{ + name: 'setStrokeStyle', + args: ['rgba(0,0,0,0.1)'] + }, { + name: 'setLineWidth', + args: [1] + }, { + name: 'setFillStyle', + args: ['rgba(0,0,0,0.1)'] + }, { + name: 'beginPath', + args: [] + }, { + name: 'arc', + args: [10, 15, 2, 0, 2 * Math.PI] + }, { + name: 'closePath', + args: [], + }, { + name: 'fill', + args: [], + }, { + name: 'stroke', + args: [] + }]); }); it ('should not draw if skipped', function() { diff --git a/test/specs/global.defaults.tests.js b/test/specs/global.defaults.tests.js index a01284c1a0b..dc4beff58e2 100644 --- a/test/specs/global.defaults.tests.js +++ b/test/specs/global.defaults.tests.js @@ -128,21 +128,27 @@ describe('Default Configs', function() { hidden: false, index: 0, strokeStyle: '#000', - lineWidth: 2 + lineWidth: 2, + legendSymbol: 'rect', + boxWidth: 40 }, { text: 'label2', fillStyle: 'green', hidden: false, index: 1, strokeStyle: '#000', - lineWidth: 2 + lineWidth: 2, + legendSymbol: 'rect', + boxWidth: 40 }, { text: 'label3', fillStyle: 'blue', hidden: true, index: 2, strokeStyle: '#000', - lineWidth: 2 + lineWidth: 2, + legendSymbol: 'rect', + boxWidth: 40 }]; expect(chart.legend.legendItems).toEqual(expected); }); @@ -244,21 +250,27 @@ describe('Default Configs', function() { hidden: false, index: 0, strokeStyle: '#000', - lineWidth: 2 + lineWidth: 2, + legendSymbol: 'rect', + boxWidth: 40 }, { text: 'label2', fillStyle: 'green', hidden: false, index: 1, strokeStyle: '#000', - lineWidth: 2 + lineWidth: 2, + legendSymbol: 'rect', + boxWidth: 40 }, { text: 'label3', fillStyle: 'blue', hidden: true, index: 2, strokeStyle: '#000', - lineWidth: 2 + lineWidth: 2, + legendSymbol: 'rect', + boxWidth: 40 }]; expect(chart.legend.legendItems).toEqual(expected); }); diff --git a/test/specs/plugin.legend.tests.js b/test/specs/plugin.legend.tests.js index 5b75069aaea..189a43c0ca1 100644 --- a/test/specs/plugin.legend.tests.js +++ b/test/specs/plugin.legend.tests.js @@ -13,6 +13,7 @@ describe('Legend block tests', function() { onHover: null, labels: { + symbol: 'rect', boxWidth: 40, padding: 10, generateLabels: jasmine.any(Function) @@ -35,6 +36,9 @@ describe('Legend block tests', function() { label: 'dataset2', hidden: true, borderJoinStyle: 'miter', + legend: { + symbol: 'line', + }, data: [] }, { label: 'dataset3', @@ -58,7 +62,9 @@ describe('Legend block tests', function() { lineWidth: undefined, strokeStyle: undefined, pointStyle: undefined, - datasetIndex: 0 + datasetIndex: 0, + legendSymbol: 'rect', + boxWidth: 40 }, { text: 'dataset2', fillStyle: undefined, @@ -70,7 +76,9 @@ describe('Legend block tests', function() { lineWidth: undefined, strokeStyle: undefined, pointStyle: undefined, - datasetIndex: 1 + datasetIndex: 1, + legendSymbol: 'line', + boxWidth: 40 }, { text: 'dataset3', fillStyle: undefined, @@ -82,7 +90,9 @@ describe('Legend block tests', function() { lineWidth: 10, strokeStyle: 'green', pointStyle: 'crossRot', - datasetIndex: 2 + datasetIndex: 2, + legendSymbol: 'rect', + boxWidth: 40 }]); }); @@ -96,6 +106,9 @@ describe('Legend block tests', function() { borderCapStyle: 'butt', borderDash: [2, 2], borderDashOffset: 5.5, + legend: { + boxWidth: 12 + }, data: [] }, { label: 'dataset2', @@ -115,6 +128,7 @@ describe('Legend block tests', function() { options: { legend: { labels: { + symbol: 'line', filter: function(legendItem, data) { var dataset = data.datasets[legendItem.datasetIndex]; return !dataset.legendHidden; @@ -135,7 +149,9 @@ describe('Legend block tests', function() { lineWidth: undefined, strokeStyle: undefined, pointStyle: undefined, - datasetIndex: 0 + datasetIndex: 0, + legendSymbol: 'line', + boxWidth: 12 }, { text: 'dataset3', fillStyle: undefined, @@ -147,7 +163,9 @@ describe('Legend block tests', function() { lineWidth: 10, strokeStyle: 'green', pointStyle: 'crossRot', - datasetIndex: 2 + datasetIndex: 2, + legendSymbol: 'line', + boxWidth: 40 }]); });