From 1d76be99907987cc6801f04613c0fb923b8745af Mon Sep 17 00:00:00 2001 From: Dominic Jean Date: Sat, 7 Oct 2017 11:50:17 -0400 Subject: [PATCH 01/11] Add functionality to display all point style symbols as legend symbol. Symbol type,, boxWidth, border Color and border Width can be defined at dataset level --- docs/charts/bar.md | 13 +-- docs/charts/bubble.md | 1 + docs/charts/doughnut.md | 1 + docs/charts/line.md | 1 + docs/charts/polar.md | 1 + docs/charts/radar.md | 11 ++- docs/configuration/legend.md | 26 ++++- src/controllers/controller.doughnut.js | 9 +- src/controllers/controller.polarArea.js | 9 +- src/helpers/helpers.canvas.js | 121 +++++++++++------------- src/plugins/plugin.legend.js | 60 ++++-------- test/specs/element.point.tests.js | 79 ++++++++-------- test/specs/global.defaults.tests.js | 24 +++-- test/specs/plugin.legend.tests.js | 28 +++++- 14 files changed, 208 insertions(+), 176 deletions(-) diff --git a/docs/charts/bar.md b/docs/charts/bar.md index 524c795214c..8698c644bbc 100644 --- a/docs/charts/bar.md +++ b/docs/charts/bar.md @@ -6,12 +6,12 @@ A bar chart provides a way of showing data values represented as vertical bars. "type": "bar", "data": { "labels": [ - "January", - "February", - "March", - "April", - "May", - "June", + "January", + "February", + "March", + "April", + "May", + "June", "July" ], "datasets": [{ @@ -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 4cb2ee6994f..b5883c2743d 100644 --- a/docs/charts/bubble.md +++ b/docs/charts/bubble.md @@ -52,6 +52,7 @@ The bubble chart allows a number of properties to be specified for each dataset. | [`label`](#labeling) | `String` | - | - | `undefined` | [`pointStyle`](#styling) | `String` | Yes | Yes | `circle` | [`radius`](#styling) | `Number` | Yes | Yes | `3` +| [`legend`](../configuration/legend.md/#dataset-legend-configuration) | `object` | Yes | - | ### Labeling diff --git a/docs/charts/doughnut.md b/docs/charts/doughnut.md index 35a7dbb56fe..60fd419dd2d 100644 --- a/docs/charts/doughnut.md +++ b/docs/charts/doughnut.md @@ -62,6 +62,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 cd4141a49a0..2d85ac61ab2 100644 --- a/docs/charts/line.md +++ b/docs/charts/line.md @@ -71,6 +71,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 29952cff672..d11d199dfb7 100644 --- a/docs/charts/polar.md +++ b/docs/charts/polar.md @@ -53,6 +53,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 ac908315cf9..de8e30b2195 100644 --- a/docs/charts/radar.md +++ b/docs/charts/radar.md @@ -8,9 +8,9 @@ They are often useful for comparing the points of two or more different data set "type": "radar", "data": { "labels": [ - "Eating", - "Drinking", - "Sleeping", + "Eating", + "Drinking", + "Sleeping", "Designing", "Coding", "Cycling", @@ -88,13 +88,14 @@ 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: * 'circle' * 'cross' * 'crossRot' -* 'dash'. +* 'dash'. * 'line' * 'rect' * 'rectRounded' @@ -127,7 +128,7 @@ It is common to want to apply a configuration setting to all created radar chart ## Data Structure -The `data` property of a dataset for a radar chart is specified as a an array of numbers. Each point in the data array corresponds to the label at the same index on the x axis. +The `data` property of a dataset for a radar chart is specified as a an array of numbers. Each point in the data array corresponds to the label at the same index on the x axis. ```javascript data: [20, 10] diff --git a/docs/configuration/legend.md b/docs/configuration/legend.md index 4cad48b556e..4fba028791d 100644 --- a/docs/configuration/legend.md +++ b/docs/configuration/legend.md @@ -10,7 +10,7 @@ The legend configuration is passed into the `options.legend` namespace. The glob | `display` | `Boolean` | `true` | is the legend 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 +| `onClick` | `Function` | | A callback that is called when a click event is registered on a label item | `onHover` | `Function` | | A callback that is called when a 'mousemove' event is registered on top of a label item | `reverse` | `Boolean` | `false` | Legend will show datasets in reverse order. | `labels` | `Object` | | See the [Legend Label Configuration](#legend-label-configuration) section below. @@ -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,3 +171,18 @@ var chart = new Chart(ctx, { } }); ``` + +### 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 6028602c472..9fd3710ee95 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 e59aedfb98a..128ccf3696c 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 13b110268b9..4d5933959be 100644 --- a/src/helpers/helpers.canvas.js +++ b/src/helpers/helpers.canvas.js @@ -45,111 +45,102 @@ var exports = module.exports = { }, drawPoint: function(ctx, style, radius, x, y) { - var type, edgeLength, xOffset, yOffset, height, size; + // call draw Symbol with converted radius to width and height + // and move x, y to the top left corner + if (this.drawSymbol(ctx, style, radius * 2, radius * 2, x - radius, y - radius)) { + // Only Stroke when return true + ctx.stroke(); + } + }, + + drawSymbol: function(ctx, style, width, height, x, y) { 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; } - + ctx.beginPath(); switch (style) { - // Default includes circle + // Default circle default: - ctx.beginPath(); - ctx.arc(x, y, radius, 0, Math.PI * 2); + // display standard circle if height and width are the same otherwise display a RectRounded + if (width === height) { + ctx.arc(x + width / 2, y + width / 2, width / 2, 0, Math.PI * 2); + } else { + this.roundedRect(ctx, x, y, width, height, width / 2); + } ctx.closePath(); ctx.fill(); break; - case 'triangle': - ctx.beginPath(); - edgeLength = 3 * radius / Math.sqrt(3); - height = edgeLength * Math.sqrt(3) / 2; - ctx.moveTo(x - edgeLength / 2, y + height / 3); - ctx.lineTo(x + edgeLength / 2, y + height / 3); - ctx.lineTo(x, y - 2 * height / 3); + case 'rect': + ctx.rect(x, y, width, height); ctx.closePath(); ctx.fill(); break; - case 'rect': - size = 1 / Math.SQRT2 * radius; - ctx.beginPath(); - ctx.fillRect(x - size, y - size, 2 * size, 2 * size); - ctx.strokeRect(x - size, y - size, 2 * size, 2 * size); + case 'triangle': + ctx.moveTo(x, y + height); + ctx.lineTo(x + width / 2, y); + ctx.lineTo(x + width, y + height); + ctx.closePath(); + ctx.fill(); break; case 'rectRounded': - var offset = radius / Math.SQRT2; - var leftX = x - offset; - var topY = y - offset; - var sideSize = Math.SQRT2 * radius; - ctx.beginPath(); - this.roundedRect(ctx, leftX, topY, sideSize, sideSize, radius / 2); + this.roundedRect(ctx, x, y, width, height, height * Math.SQRT2 / 4); ctx.closePath(); ctx.fill(); break; case 'rectRot': - size = 1 / Math.SQRT2 * radius; - ctx.beginPath(); - ctx.moveTo(x - size, y); - ctx.lineTo(x, y + size); - ctx.lineTo(x + size, y); - ctx.lineTo(x, y - size); + ctx.moveTo(x, y + height / 2); + ctx.lineTo(x + width / 2, y); + ctx.lineTo(x + width, y + height / 2); + ctx.lineTo(x + width / 2, y + height); ctx.closePath(); ctx.fill(); break; case 'cross': - ctx.beginPath(); - ctx.moveTo(x, y + radius); - ctx.lineTo(x, y - radius); - ctx.moveTo(x - radius, y); - ctx.lineTo(x + radius, y); + ctx.moveTo(x + width / 2, y); + ctx.lineTo(x + width / 2, y + height); + ctx.moveTo(x, y + height / 2); + ctx.lineTo(x + width, y + height / 2); ctx.closePath(); break; case 'crossRot': - ctx.beginPath(); - xOffset = Math.cos(Math.PI / 4) * radius; - yOffset = Math.sin(Math.PI / 4) * radius; - ctx.moveTo(x - xOffset, y - yOffset); - ctx.lineTo(x + xOffset, y + yOffset); - ctx.moveTo(x - xOffset, y + yOffset); - ctx.lineTo(x + xOffset, y - yOffset); + ctx.moveTo(x, y); + ctx.lineTo(x + width, y + height); + ctx.moveTo(x, y + height); + ctx.lineTo(x + width, y); ctx.closePath(); break; case 'star': - ctx.beginPath(); - ctx.moveTo(x, y + radius); - ctx.lineTo(x, y - radius); - ctx.moveTo(x - radius, y); - ctx.lineTo(x + radius, y); - xOffset = Math.cos(Math.PI / 4) * radius; - yOffset = Math.sin(Math.PI / 4) * radius; - ctx.moveTo(x - xOffset, y - yOffset); - ctx.lineTo(x + xOffset, y + yOffset); - ctx.moveTo(x - xOffset, y + yOffset); - ctx.lineTo(x + xOffset, y - yOffset); + ctx.moveTo(x + width / 2, y); + ctx.lineTo(x + width / 2, y + height); + ctx.moveTo(x, y + height / 2); + ctx.lineTo(x + width, y + height / 2); + ctx.moveTo(x, y); + ctx.lineTo(x + width, y + height); + ctx.moveTo(x, y + height); + ctx.lineTo(x + width, y); ctx.closePath(); break; case 'line': - ctx.beginPath(); - ctx.moveTo(x - radius, y); - ctx.lineTo(x + radius, y); + ctx.moveTo(x, y + height / 2); + ctx.lineTo(x + width, y + height / 2); ctx.closePath(); break; case 'dash': - ctx.beginPath(); - ctx.moveTo(x, y); - ctx.lineTo(x + radius, y); + ctx.moveTo(x + width / 2, y + height / 2); + ctx.lineTo(x + width, y + height / 2); ctx.closePath(); break; } + return true; - ctx.stroke(); }, clipArea: function(ctx, area) { diff --git a/src/plugins/plugin.legend.js b/src/plugins/plugin.legend.js index 71e0e4110e4..275e3d4f228 100644 --- a/src/plugins/plugin.legend.js +++ b/src/plugins/plugin.legend.js @@ -11,7 +11,6 @@ defaults._set('global', { fullWidth: true, reverse: false, weight: 1000, - // a callback that will handle onClick: function(e, legendItem) { var index = legendItem.datasetIndex; @@ -28,6 +27,7 @@ defaults._set('global', { onHover: null, labels: { + symbol: 'rect', boxWidth: 40, padding: 10, // Generates labels shown in the legend @@ -41,8 +41,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, @@ -52,10 +57,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 }; @@ -84,18 +90,6 @@ module.exports = function(Chart) { var layout = Chart.layoutService; var noop = helpers.noop; - /** - * 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; - } - Chart.Legend = Element.extend({ initialize: function(config) { @@ -246,8 +240,7 @@ module.exports = function(Chart) { 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); @@ -276,8 +269,7 @@ module.exports = function(Chart) { 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) { @@ -345,12 +337,11 @@ module.exports = function(Chart) { 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; } @@ -369,30 +360,15 @@ module.exports = function(Chart) { // IE 9 and 10 do not support line dash 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); @@ -426,7 +402,7 @@ module.exports = function(Chart) { 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 f09b912d3c0..3aa3c436b6c 100644 --- a/test/specs/element.point.tests.js +++ b/test/specs/element.point.tests.js @@ -116,6 +116,9 @@ describe('Point element tests', function() { 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; point.draw(); @@ -163,13 +166,13 @@ describe('Point element tests', function() { args: [] }, { name: 'moveTo', - args: [10 - 3 * 2 / Math.sqrt(3) / 2, 15 + 3 * 2 / Math.sqrt(3) * Math.sqrt(3) / 2 / 3] + args: [tx, ty + tw] }, { name: 'lineTo', - args: [10 + 3 * 2 / Math.sqrt(3) / 2, 15 + 3 * 2 / Math.sqrt(3) * Math.sqrt(3) / 2 / 3], + args: [tx + tw / 2, ty], }, { name: 'lineTo', - args: [10, 15 - 2 * 3 * 2 / Math.sqrt(3) * Math.sqrt(3) / 2 / 3], + args: [tx + tw, ty + tw], }, { name: 'closePath', args: [], @@ -198,11 +201,14 @@ describe('Point element tests', function() { name: 'beginPath', args: [] }, { - name: 'fillRect', - args: [10 - 1 / Math.SQRT2 * 2, 15 - 1 / Math.SQRT2 * 2, 2 / Math.SQRT2 * 2, 2 / Math.SQRT2 * 2] + name: 'rect', + args: [tx, ty, tw, tw] }, { - name: 'strokeRect', - args: [10 - 1 / Math.SQRT2 * 2, 15 - 1 / Math.SQRT2 * 2, 2 / Math.SQRT2 * 2, 2 / Math.SQRT2 * 2] + name: 'closePath', + args: [], + }, { + name: 'fill', + args: [], }, { name: 'stroke', args: [] @@ -210,7 +216,6 @@ describe('Point element tests', function() { var drawRoundedRectangleSpy = jasmine.createSpy('drawRoundedRectangle'); var drawRoundedRectangle = Chart.helpers.canvas.roundedRect; - var offset = point._view.radius / Math.SQRT2; Chart.helpers.canvas.roundedRect = drawRoundedRectangleSpy; mockContext.resetCalls(); point._view.pointStyle = 'rectRounded'; @@ -218,11 +223,11 @@ describe('Point element tests', function() { expect(drawRoundedRectangleSpy).toHaveBeenCalledWith( mockContext, - 10 - offset, - 15 - offset, - Math.SQRT2 * 2, - Math.SQRT2 * 2, - 2 / 2 + tx, + ty, + tw, + tw, + tw * Math.SQRT2 / 4 ); expect(mockContext.getCalls()).toContain( jasmine.objectContaining({ @@ -250,16 +255,16 @@ describe('Point element tests', function() { args: [] }, { name: 'moveTo', - args: [10 - 1 / Math.SQRT2 * 2, 15] + args: [tx, ty + tw / 2] }, { name: 'lineTo', - args: [10, 15 + 1 / Math.SQRT2 * 2] + args: [tx + tw / 2, ty] }, { name: 'lineTo', - args: [10 + 1 / Math.SQRT2 * 2, 15], + args: [tx + tw, ty + tw / 2], }, { name: 'lineTo', - args: [10, 15 - 1 / Math.SQRT2 * 2], + args: [tx + tw / 2, ty + tw], }, { name: 'closePath', args: [] @@ -289,16 +294,16 @@ describe('Point element tests', function() { args: [] }, { name: 'moveTo', - args: [10, 17] + args: [tx + tw / 2, ty] }, { name: 'lineTo', - args: [10, 13], + args: [tx + tw / 2, ty + tw], }, { name: 'moveTo', - args: [8, 15], + args: [tx, ty + tw / 2], }, { name: 'lineTo', - args: [12, 15], + args: [tx + tw, ty + tw / 2], }, { name: 'closePath', args: [], @@ -325,16 +330,16 @@ describe('Point element tests', function() { args: [] }, { name: 'moveTo', - args: [10 - Math.cos(Math.PI / 4) * 2, 15 - Math.sin(Math.PI / 4) * 2] + args: [tx, ty] }, { name: 'lineTo', - args: [10 + Math.cos(Math.PI / 4) * 2, 15 + Math.sin(Math.PI / 4) * 2], + args: [tx + tw, ty + tw], }, { name: 'moveTo', - args: [10 - Math.cos(Math.PI / 4) * 2, 15 + Math.sin(Math.PI / 4) * 2], + args: [tx, ty + tw], }, { name: 'lineTo', - args: [10 + Math.cos(Math.PI / 4) * 2, 15 - Math.sin(Math.PI / 4) * 2], + args: [tx + tw, ty], }, { name: 'closePath', args: [], @@ -361,28 +366,28 @@ describe('Point element tests', function() { args: [] }, { name: 'moveTo', - args: [10, 17] + args: [tx + tw / 2, ty] }, { name: 'lineTo', - args: [10, 13], + args: [tx + tw / 2, ty + tw], }, { name: 'moveTo', - args: [8, 15], + args: [tx, ty + tw / 2], }, { name: 'lineTo', - args: [12, 15], + args: [tx + tw, ty + tw / 2], }, { name: 'moveTo', - args: [10 - Math.cos(Math.PI / 4) * 2, 15 - Math.sin(Math.PI / 4) * 2] + args: [tx, ty] }, { name: 'lineTo', - args: [10 + Math.cos(Math.PI / 4) * 2, 15 + Math.sin(Math.PI / 4) * 2], + args: [tx + tw, ty + tw], }, { name: 'moveTo', - args: [10 - Math.cos(Math.PI / 4) * 2, 15 + Math.sin(Math.PI / 4) * 2], + args: [tx, ty + tw], }, { name: 'lineTo', - args: [10 + Math.cos(Math.PI / 4) * 2, 15 - Math.sin(Math.PI / 4) * 2], + args: [tx + tw, ty], }, { name: 'closePath', args: [], @@ -409,10 +414,10 @@ describe('Point element tests', function() { args: [] }, { name: 'moveTo', - args: [8, 15] + args: [tx, ty + tw / 2] }, { name: 'lineTo', - args: [12, 15], + args: [tx + tw, ty + tw / 2], }, { name: 'closePath', args: [], @@ -439,10 +444,10 @@ describe('Point element tests', function() { args: [] }, { name: 'moveTo', - args: [10, 15] + args: [tx + tw / 2, ty + tw / 2] }, { name: 'lineTo', - args: [12, 15], + args: [tx + tw, ty + tw / 2], }, { name: 'closePath', args: [], diff --git a/test/specs/global.defaults.tests.js b/test/specs/global.defaults.tests.js index 0e859901434..dd59c8fab78 100644 --- a/test/specs/global.defaults.tests.js +++ b/test/specs/global.defaults.tests.js @@ -129,21 +129,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); }); @@ -245,21 +251,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 b950c360160..9e5518b4c05 100644 --- a/test/specs/plugin.legend.tests.js +++ b/test/specs/plugin.legend.tests.js @@ -18,6 +18,7 @@ describe('Legend block tests', function() { onHover: null, labels: { + symbol: 'rect', boxWidth: 40, padding: 10, generateLabels: jasmine.any(Function) @@ -40,6 +41,9 @@ describe('Legend block tests', function() { label: 'dataset2', hidden: true, borderJoinStyle: 'miter', + legend: { + symbol: 'line', + }, data: [] }, { label: 'dataset3', @@ -63,7 +67,9 @@ describe('Legend block tests', function() { lineWidth: undefined, strokeStyle: undefined, pointStyle: undefined, - datasetIndex: 0 + datasetIndex: 0, + legendSymbol: 'rect', + boxWidth: 40 }, { text: 'dataset2', fillStyle: undefined, @@ -75,7 +81,9 @@ describe('Legend block tests', function() { lineWidth: undefined, strokeStyle: undefined, pointStyle: undefined, - datasetIndex: 1 + datasetIndex: 1, + legendSymbol: 'line', + boxWidth: 40 }, { text: 'dataset3', fillStyle: undefined, @@ -87,7 +95,9 @@ describe('Legend block tests', function() { lineWidth: 10, strokeStyle: 'green', pointStyle: 'crossRot', - datasetIndex: 2 + datasetIndex: 2, + legendSymbol: 'rect', + boxWidth: 40 }]); }); @@ -101,6 +111,9 @@ describe('Legend block tests', function() { borderCapStyle: 'butt', borderDash: [2, 2], borderDashOffset: 5.5, + legend: { + boxWidth: 12 + }, data: [] }, { label: 'dataset2', @@ -120,6 +133,7 @@ describe('Legend block tests', function() { options: { legend: { labels: { + symbol: 'line', filter: function(legendItem, data) { var dataset = data.datasets[legendItem.datasetIndex]; return !dataset.legendHidden; @@ -140,7 +154,9 @@ describe('Legend block tests', function() { lineWidth: undefined, strokeStyle: undefined, pointStyle: undefined, - datasetIndex: 0 + datasetIndex: 0, + legendSymbol: 'line', + boxWidth: 12 }, { text: 'dataset3', fillStyle: undefined, @@ -152,7 +168,9 @@ describe('Legend block tests', function() { lineWidth: 10, strokeStyle: 'green', pointStyle: 'crossRot', - datasetIndex: 2 + datasetIndex: 2, + legendSymbol: 'line', + boxWidth: 40 }]); }); From 3d85b788245ad236e3be153268c777efeef105ca Mon Sep 17 00:00:00 2001 From: touletan Date: Sat, 7 Oct 2017 12:45:30 -0400 Subject: [PATCH 02/11] Update bubble.md --- docs/charts/bubble.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/charts/bubble.md b/docs/charts/bubble.md index b5883c2743d..79a27a2a7bb 100644 --- a/docs/charts/bubble.md +++ b/docs/charts/bubble.md @@ -52,7 +52,7 @@ The bubble chart allows a number of properties to be specified for each dataset. | [`label`](#labeling) | `String` | - | - | `undefined` | [`pointStyle`](#styling) | `String` | Yes | Yes | `circle` | [`radius`](#styling) | `Number` | Yes | Yes | `3` -| [`legend`](../configuration/legend.md/#dataset-legend-configuration) | `object` | Yes | - | +| [`legend`](../configuration/legend.md/#dataset-legend-configuration) | `object` | - | - | ### Labeling From 13eabb9cb044e6b34c72389d572ae18cd223b00e Mon Sep 17 00:00:00 2001 From: touletan Date: Tue, 16 Jan 2018 23:19:36 -0500 Subject: [PATCH 03/11] rebase and fix merge conflicts --- .codeclimate.yml | 32 +- .eslintrc | 224 ----- .eslintrc.yml | 5 + .gitignore | 2 + .travis.yml | 2 +- LICENSE.md | 2 +- README.md | 2 +- docs/SUMMARY.md | 1 + docs/axes/cartesian/README.md | 6 +- docs/axes/cartesian/linear.md | 4 +- docs/axes/cartesian/time.md | 2 +- docs/axes/labelling.md | 6 +- docs/axes/radial/linear.md | 12 +- docs/axes/styling.md | 14 +- docs/charts/bar.md | 2 +- docs/charts/line.md | 4 +- docs/configuration/legend.md | 3 +- docs/configuration/title.md | 6 +- docs/configuration/tooltip.md | 38 +- docs/developers/README.md | 7 +- docs/developers/axes.md | 4 +- docs/developers/updates.md | 76 +- docs/getting-started/README.md | 2 +- docs/getting-started/installation.md | 17 +- docs/notes/extensions.md | 11 +- gulpfile.js | 32 +- karma.conf.js | 8 +- package.json | 32 +- samples/samples.js | 3 + samples/scales/gridlines-display.html | 2 +- samples/scales/gridlines-style.html | 2 +- samples/scales/toggle-scale-type.html | 99 +++ src/chart.js | 66 +- src/controllers/controller.bar.js | 179 ++-- src/core/core.controller.js | 133 ++- src/core/core.datasetController.js | 4 +- src/core/core.helpers.js | 38 +- src/core/core.layoutService.js | 422 ---------- src/core/core.layouts.js | 419 ++++++++++ src/core/core.plugin.js | 374 --------- src/core/core.plugins.js | 382 +++++++++ src/core/core.scaleService.js | 3 +- src/core/core.ticks.js | 134 --- src/core/core.tooltip.js | 47 +- src/elements/element.point.js | 4 +- src/helpers/helpers.core.js | 42 + src/platforms/platform.dom.js | 7 + src/plugins/index.js | 6 + src/plugins/plugin.filler.js | 485 ++++++----- src/plugins/plugin.legend.js | 780 +++++++++--------- src/plugins/plugin.title.js | 411 ++++----- src/scales/scale.linear.js | 7 +- src/scales/scale.linearbase.js | 58 +- src/scales/scale.logarithmic.js | 239 ++++-- src/scales/scale.time.js | 103 ++- test/{.eslintrc => .eslintrc.yml} | 1 + .../bar-thickness-absolute.json | 42 + .../controller.bar/bar-thickness-absolute.png | Bin 0 -> 5055 bytes .../bar-thickness-flex-offset.json | 42 + .../bar-thickness-flex-offset.png | Bin 0 -> 4583 bytes .../controller.bar/bar-thickness-flex.json | 41 + .../controller.bar/bar-thickness-flex.png | Bin 0 -> 5095 bytes .../controller.bar/bar-thickness-max.json | 41 + .../controller.bar/bar-thickness-max.png | Bin 0 -> 4421 bytes .../bar-thickness-min-interval.json | 40 + .../bar-thickness-min-interval.png | Bin 0 -> 5180 bytes .../bar-thickness-multiple.json | 46 ++ .../controller.bar/bar-thickness-multiple.png | Bin 0 -> 5847 bytes .../bar-thickness-no-overlap.json | 46 ++ .../bar-thickness-no-overlap.png | Bin 0 -> 4211 bytes .../controller.bar/bar-thickness-offset.json | 47 ++ .../controller.bar/bar-thickness-offset.png | Bin 0 -> 6577 bytes .../bar-thickness-single-xy.json | 40 + .../bar-thickness-single-xy.png | Bin 0 -> 4514 bytes .../controller.bar/bar-thickness-single.json | 43 + .../controller.bar/bar-thickness-single.png | Bin 0 -> 4374 bytes .../controller.bar/bar-thickness-stacked.json | 48 ++ .../controller.bar/bar-thickness-stacked.png | Bin 0 -> 5586 bytes test/jasmine.utils.js | 11 +- test/specs/controller.bar.tests.js | 82 +- test/specs/controller.scatter.test.js | 25 + test/specs/core.controller.tests.js | 200 ++++- test/specs/core.helpers.tests.js | 37 +- ...Service.tests.js => core.layouts.tests.js} | 43 +- test/specs/core.plugin.tests.js | 33 + test/specs/core.ticks.tests.js | 96 +++ test/specs/core.tooltip.tests.js | 129 ++- test/specs/global.deprecations.tests.js | 31 +- test/specs/helpers.core.tests.js | 53 ++ test/specs/plugin.legend.tests.js | 5 - test/specs/plugin.title.tests.js | 5 - test/specs/scale.logarithmic.tests.js | 489 +++++++++-- test/specs/scale.time.tests.js | 166 +++- 93 files changed, 4358 insertions(+), 2529 deletions(-) delete mode 100644 .eslintrc create mode 100644 .eslintrc.yml create mode 100644 samples/scales/toggle-scale-type.html delete mode 100644 src/core/core.layoutService.js create mode 100644 src/core/core.layouts.js delete mode 100644 src/core/core.plugin.js create mode 100644 src/core/core.plugins.js create mode 100644 src/plugins/index.js rename test/{.eslintrc => .eslintrc.yml} (89%) create mode 100644 test/fixtures/controller.bar/bar-thickness-absolute.json create mode 100644 test/fixtures/controller.bar/bar-thickness-absolute.png create mode 100644 test/fixtures/controller.bar/bar-thickness-flex-offset.json create mode 100644 test/fixtures/controller.bar/bar-thickness-flex-offset.png create mode 100644 test/fixtures/controller.bar/bar-thickness-flex.json create mode 100644 test/fixtures/controller.bar/bar-thickness-flex.png create mode 100644 test/fixtures/controller.bar/bar-thickness-max.json create mode 100644 test/fixtures/controller.bar/bar-thickness-max.png create mode 100644 test/fixtures/controller.bar/bar-thickness-min-interval.json create mode 100644 test/fixtures/controller.bar/bar-thickness-min-interval.png create mode 100644 test/fixtures/controller.bar/bar-thickness-multiple.json create mode 100644 test/fixtures/controller.bar/bar-thickness-multiple.png create mode 100644 test/fixtures/controller.bar/bar-thickness-no-overlap.json create mode 100644 test/fixtures/controller.bar/bar-thickness-no-overlap.png create mode 100644 test/fixtures/controller.bar/bar-thickness-offset.json create mode 100644 test/fixtures/controller.bar/bar-thickness-offset.png create mode 100644 test/fixtures/controller.bar/bar-thickness-single-xy.json create mode 100644 test/fixtures/controller.bar/bar-thickness-single-xy.png create mode 100644 test/fixtures/controller.bar/bar-thickness-single.json create mode 100644 test/fixtures/controller.bar/bar-thickness-single.png create mode 100644 test/fixtures/controller.bar/bar-thickness-stacked.json create mode 100644 test/fixtures/controller.bar/bar-thickness-stacked.png create mode 100644 test/specs/controller.scatter.test.js rename test/specs/{core.layoutService.tests.js => core.layouts.tests.js} (92%) create mode 100644 test/specs/core.ticks.tests.js diff --git a/.codeclimate.yml b/.codeclimate.yml index fcc885c8200..0b8340feb58 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,25 +1,19 @@ -engines: +version: "2" +plugins: duplication: enabled: true config: languages: - - javascript - eslint: - enabled: true - channel: "eslint-3" + - javascript fixme: enabled: true -ratings: - paths: - - "src/**/*.js" -exclude_paths: -- '.github/' -- 'dist/' -- 'test/' -- 'docs/' -- 'samples/' -- 'scripts/' -- '**.md' -- '**.json' -- 'gulpfile.js' -- 'karma.conf.js' +exclude_patterns: + - "dist/" + - "docs/" + - "samples/" + - "scripts/" + - "test/" + - "*.js" + - "*.json" + - "*.md" + - ".*" diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 6a31e4b6ec2..00000000000 --- a/.eslintrc +++ /dev/null @@ -1,224 +0,0 @@ -ecmaFeatures: - modules: true - jsx: true - -env: - amd: true - browser: true - es6: true - jquery: true - node: true - -# http://eslint.org/docs/rules/ -rules: - # Possible Errors - no-cond-assign: 2 - no-console: [2, {allow: [warn, error]}] - no-constant-condition: 2 - no-control-regex: 2 - no-debugger: 2 - no-dupe-args: 2 - no-dupe-keys: 2 - no-duplicate-case: 2 - no-empty: 2 - no-empty-character-class: 2 - no-ex-assign: 2 - no-extra-boolean-cast: 2 - no-extra-parens: [2, functions] - no-extra-semi: 2 - no-func-assign: 2 - no-inner-declarations: [2, functions] - no-invalid-regexp: 2 - no-irregular-whitespace: 2 - no-negated-in-lhs: 2 - no-obj-calls: 2 - no-regex-spaces: 2 - no-sparse-arrays: 2 - no-unexpected-multiline: 2 - no-unreachable: 2 - use-isnan: 2 - valid-jsdoc: 0 - valid-typeof: 2 - - # Best Practices - accessor-pairs: 2 - array-callback-return: 0 - block-scoped-var: 0 - complexity: [2, 10] - consistent-return: 0 - curly: [2, all] - default-case: 2 - dot-location: 0 - dot-notation: 2 - eqeqeq: 2 - guard-for-in: 2 - no-alert: 2 - no-caller: 2 - no-case-declarations: 2 - no-div-regex: 2 - no-else-return: 2 - no-empty-pattern: 2 - no-eq-null: 2 - no-eval: 2 - no-extend-native: 2 - no-extra-bind: 2 - no-fallthrough: 2 - no-floating-decimal: 2 - no-implicit-coercion: 0 - no-implied-eval: 2 - no-invalid-this: 0 - no-iterator: 2 - no-labels: 2 - no-lone-blocks: 2 - no-loop-func: 2 - no-magic-number: 0 - no-multi-spaces: 2 - no-multi-str: 2 - no-native-reassign: 2 - no-new-func: 2 - no-new-wrappers: 2 - no-new: 2 - no-octal-escape: 2 - no-octal: 2 - no-proto: 2 - no-redeclare: 2 - no-return-assign: 2 - no-script-url: 2 - no-self-compare: 2 - no-sequences: 2 - no-throw-literal: 0 - no-unused-expressions: 2 - no-useless-call: 2 - no-useless-concat: 2 - no-void: 2 - no-warning-comments: 0 - no-with: 2 - radix: 2 - vars-on-top: 0 - wrap-iife: 2 - yoda: [1, never] - - # Strict - strict: 0 - - # Variables - init-declarations: 0 - no-catch-shadow: 2 - no-delete-var: 2 - no-label-var: 2 - no-shadow-restricted-names: 2 - no-shadow: 2 - no-undef-init: 2 - no-undef: 2 - no-undefined: 0 - no-unused-vars: 2 - no-use-before-define: 2 - - # Node.js and CommonJS - callback-return: 2 - global-require: 2 - handle-callback-err: 2 - no-mixed-requires: 0 - no-new-require: 0 - no-path-concat: 2 - no-process-exit: 2 - no-restricted-modules: 0 - no-sync: 0 - - # Stylistic Issues - array-bracket-spacing: [2, never] - block-spacing: 0 - brace-style: [2, 1tbs] - camelcase: 2 - comma-dangle: [2, only-multiline] - comma-spacing: 2 - comma-style: [2, last] - computed-property-spacing: [2, never] - consistent-this: [2, me] - eol-last: 2 - func-call-spacing: 0 - func-names: [2, never] - func-style: 0 - id-length: 0 - id-match: 0 - indent: [2, tab] - jsx-quotes: 0 - key-spacing: 2 - keyword-spacing: 2 - linebreak-style: 0 - lines-around-comment: 0 - max-depth: 0 - max-len: 0 - max-lines: 0 - max-nested-callbacks: 0 - max-params: 0 - max-statements-per-line: 0 - max-statements: [2, 30] - multiline-ternary: 0 - new-cap: 0 - new-parens: 2 - newline-after-var: 0 - newline-before-return: 0 - newline-per-chained-call: 0 - no-array-constructor: 0 - no-bitwise: 0 - no-continue: 0 - no-inline-comments: 0 - no-lonely-if: 2 - no-mixed-operators: 0 - no-mixed-spaces-and-tabs: 2 - no-multiple-empty-lines: [2, {max: 2}] - no-negated-condition: 0 - no-nested-ternary: 0 - no-new-object: 0 - no-plusplus: 0 - no-restricted-syntax: 0 - no-spaced-func: 0 - no-ternary: 0 - no-trailing-spaces: 2 - no-underscore-dangle: 0 - no-unneeded-ternary: 0 - no-whitespace-before-property: 2 - object-curly-newline: 0 - object-curly-spacing: [2, never] - object-property-newline: 0 - one-var-declaration-per-line: 2 - one-var: [2, {initialized: never}] - operator-assignment: 0 - operator-linebreak: 0 - padded-blocks: 0 - quote-props: [2, as-needed] - quotes: [2, single, {avoidEscape: true}] - require-jsdoc: 0 - semi-spacing: 2 - semi: [2, always] - sort-keys: 0 - sort-vars: 0 - space-before-blocks: [2, always] - space-before-function-paren: [2, never] - space-in-parens: [2, never] - space-infix-ops: 2 - space-unary-ops: [2, {words: true, nonwords: false}] - spaced-comment: [2, always] - unicode-bom: 0 - wrap-regex: 2 - - # ECMAScript 6 - arrow-body-style: 0 - arrow-parens: 0 - arrow-spacing: 0 - constructor-super: 0 - generator-star-spacing: 0 - no-arrow-condition: 0 - no-class-assign: 0 - no-const-assign: 0 - no-dupe-class-members: 0 - no-this-before-super: 0 - no-var: 0 - object-shorthand: 0 - prefer-arrow-callback: 0 - prefer-const: 0 - prefer-reflect: 0 - prefer-spread: 0 - prefer-template: 0 - require-yield: 0 diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 00000000000..8f9b4af3023 --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1,5 @@ +extends: chartjs + +env: + browser: true + node: true diff --git a/.gitignore b/.gitignore index 53ce8fedb1c..0a65be9b282 100644 --- a/.gitignore +++ b/.gitignore @@ -6,9 +6,11 @@ /gh-pages /jsdoc /node_modules +/package-lock.json .DS_Store .idea .vscode bower.json *.log *.swp +*.stackdump diff --git a/.travis.yml b/.travis.yml index 862d8b0445a..9b38ab3c146 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,7 @@ script: - gulp docs - gulp package - gulp bower - - cat ./coverage/lcov.info | ./node_modules/.bin/coveralls + - cat ./coverage/lcov.info | ./node_modules/.bin/coveralls || true notifications: slack: chartjs:pcfCZR6ugg5TEcaLtmIfQYuA diff --git a/LICENSE.md b/LICENSE.md index 620db307e3c..29c941dcccf 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2013-2017 Nick Downie +Copyright (c) 2018 Chart.js Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/README.md b/README.md index 767950b88bc..10d2c1d8d7d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Chart.js -[![travis](https://img.shields.io/travis/chartjs/Chart.js.svg?style=flat-square&maxAge=60)](https://travis-ci.org/chartjs/Chart.js) [![codeclimate](https://img.shields.io/codeclimate/github/chartjs/Chart.js.svg?style=flat-square&maxAge=600)](https://codeclimate.com/github/chartjs/Chart.js) [![coveralls](https://img.shields.io/coveralls/chartjs/Chart.js.svg?style=flat-square&maxAge=600)](https://coveralls.io/github/chartjs/Chart.js?branch=master) [![slack](https://img.shields.io/badge/slack-Chart.js-blue.svg?style=flat-square&maxAge=3600)](https://chart-js-automation.herokuapp.com/) +[![travis](https://img.shields.io/travis/chartjs/Chart.js.svg?style=flat-square&maxAge=60)](https://travis-ci.org/chartjs/Chart.js) [![coveralls](https://img.shields.io/coveralls/chartjs/Chart.js.svg?style=flat-square&maxAge=600)](https://coveralls.io/github/chartjs/Chart.js?branch=master) [![codeclimate](https://img.shields.io/codeclimate/maintainability/chartjs/Chart.js.svg?style=flat-square&maxAge=600)](https://codeclimate.com/github/chartjs/Chart.js) [![slack](https://img.shields.io/badge/slack-Chart.js-blue.svg?style=flat-square&maxAge=3600)](https://chart-js-automation.herokuapp.com/) *Simple HTML5 Charts using the canvas element* [chartjs.org](http://www.chartjs.org) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 60630090003..8591dad86ae 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -7,6 +7,7 @@ * [Usage](getting-started/usage.md) * [General](general/README.md) * [Responsive](general/responsive.md) + * [Pixel Ratio](general/device-pixel-ratio.md) * [Interactions](general/interactions/README.md) * [Events](general/interactions/events.md) * [Modes](general/interactions/modes.md) diff --git a/docs/axes/cartesian/README.md b/docs/axes/cartesian/README.md index 64ce32073c2..8518da9f250 100644 --- a/docs/axes/cartesian/README.md +++ b/docs/axes/cartesian/README.md @@ -75,13 +75,13 @@ var myChart = new Chart(ctx, { data: { datasets: [{ data: [20, 50, 100, 75, 25, 0], - label: 'Left dataset' + label: 'Left dataset', // This binds the dataset to the left y axis yAxisID: 'left-y-axis' }, { - data: [0.1, 0.5, 1.0, 2.0, 1.5, 0] - label: 'Right dataset' + data: [0.1, 0.5, 1.0, 2.0, 1.5, 0], + label: 'Right dataset', // This binds the dataset to the right y axis yAxisID: 'right-y-axis', diff --git a/docs/axes/cartesian/linear.md b/docs/axes/cartesian/linear.md index 00f7aced1fe..1d0292074a8 100644 --- a/docs/axes/cartesian/linear.md +++ b/docs/axes/cartesian/linear.md @@ -20,7 +20,7 @@ The following options are provided by the linear scale. They are all located in Given the number of axis range settings, it is important to understand how they all interact with each other. -The `suggestedMax` and `suggestedMin` settings only change the data values that are used to scale the axis. These are useful for extending the range of the axis while maintaing the auto fit behaviour. +The `suggestedMax` and `suggestedMin` settings only change the data values that are used to scale the axis. These are useful for extending the range of the axis while maintaining the auto fit behaviour. ```javascript let minDataValue = Math.min(mostNegativeValue, options.ticks.suggestedMin); @@ -43,7 +43,7 @@ let chart = new Chart(ctx, { scales: { yAxes: [{ ticks: { - suggestedMin: 50 + suggestedMin: 50, suggestedMax: 100 } }] diff --git a/docs/axes/cartesian/time.md b/docs/axes/cartesian/time.md index e087d186817..9d15b5e2674 100644 --- a/docs/axes/cartesian/time.md +++ b/docs/axes/cartesian/time.md @@ -34,7 +34,7 @@ The following options are provided by the time scale. You may also set options p | `time.isoWeekday` | `Boolean` | `false` | If true and the unit is set to 'week', then the first day of the week will be Monday. Otherwise, it will be Sunday. | `time.max` | [Time](#date-formats) | | If defined, this will override the data maximum | `time.min` | [Time](#date-formats) | | If defined, this will override the data minimum -| `time.parser` | `String` or `Function` | | Custom parser for dates. [more...](#parser) +| `time.parser` | `String/Function` | | Custom parser for dates. [more...](#parser) | `time.round` | `String` | `false` | If defined, dates will be rounded to the start of this unit. See [Time Units](#time-units) below for the allowed units. | `time.tooltipFormat` | `String` | | The moment js format string to use for the tooltip. | `time.unit` | `String` | `false` | If defined, will force the unit to be a certain type. See [Time Units](#time-units) section below for details. diff --git a/docs/axes/labelling.md b/docs/axes/labelling.md index aec8d990ff4..2e8f28c805c 100644 --- a/docs/axes/labelling.md +++ b/docs/axes/labelling.md @@ -10,12 +10,12 @@ The scale label configuration is nested under the scale configuration in the `sc | -----| ---- | --------| ----------- | `display` | `Boolean` | `false` | If true, display the axis title. | `labelString` | `String` | `''` | The text for the title. (i.e. "# of People" or "Response Choices"). -| `lineHeight` | `Number|String` | `1.2` | Height of an individual line of text (see [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/line-height)) -| `fontColor` | Color | `'#666'` | Font color for scale title. +| `lineHeight` | `Number/String` | `1.2` | Height of an individual line of text (see [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/line-height)) +| `fontColor` | `Color` | `'#666'` | Font color for scale title. | `fontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | Font family for the scale title, follows CSS font-family options. | `fontSize` | `Number` | `12` | Font size for scale title. | `fontStyle` | `String` | `'normal'` | Font style for the scale title, follows CSS font-style options (i.e. normal, italic, oblique, initial, inherit). -| `padding` | `Number` or `Object` | `4` | Padding to apply around scale labels. Only `top` and `bottom` are implemented. +| `padding` | `Number/Object` | `4` | Padding to apply around scale labels. Only `top` and `bottom` are implemented. ## Creating Custom Tick Formats diff --git a/docs/axes/radial/linear.md b/docs/axes/radial/linear.md index f9811c6f985..adebe77cc61 100644 --- a/docs/axes/radial/linear.md +++ b/docs/axes/radial/linear.md @@ -20,7 +20,7 @@ The following options are provided by the linear scale. They are all located in | Name | Type | Default | Description | -----| ---- | --------| ----------- -| `backdropColor` | Color | `'rgba(255, 255, 255, 0.75)'` | Color of label backdrops +| `backdropColor` | `Color` | `'rgba(255, 255, 255, 0.75)'` | Color of label backdrops | `backdropPaddingX` | `Number` | `2` | Horizontal padding of label backdrop. | `backdropPaddingY` | `Number` | `2` | Vertical padding of label backdrop. | `beginAtZero` | `Boolean` | `false` | if true, scale will include 0 if it is not already included. @@ -36,7 +36,7 @@ The following options are provided by the linear scale. They are all located in Given the number of axis range settings, it is important to understand how they all interact with each other. -The `suggestedMax` and `suggestedMin` settings only change the data values that are used to scale the axis. These are useful for extending the range of the axis while maintaing the auto fit behaviour. +The `suggestedMax` and `suggestedMin` settings only change the data values that are used to scale the axis. These are useful for extending the range of the axis while maintaining the auto fit behaviour. ```javascript let minDataValue = Math.min(mostNegativeValue, options.ticks.suggestedMin); @@ -58,7 +58,7 @@ let chart = new Chart(ctx, { options: { scale: { ticks: { - suggestedMin: 50 + suggestedMin: 50, suggestedMax: 100 } } @@ -94,7 +94,7 @@ The following options are used to configure angled lines that radiate from the c | Name | Type | Default | Description | -----| ---- | --------| ----------- | `display` | `Boolean` | `true` | if true, angle lines are shown -| `color` | Color | `rgba(0, 0, 0, 0.1)` | Color of angled lines +| `color` | `Color` | `rgba(0, 0, 0, 0.1)` | Color of angled lines | `lineWidth` | `Number` | `1` | Width of angled lines ## Point Label Options @@ -104,7 +104,7 @@ The following options are used to configure the point labels that are shown on t | Name | Type | Default | Description | -----| ---- | --------| ----------- | `callback` | `Function` | | Callback function to transform data labels to point labels. The default implementation simply returns the current string. -| `fontColor` | Color | `'#666'` | Font color for point labels. +| `fontColor` | `Color` | `'#666'` | Font color for point labels. | `fontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | Font family to use when rendering labels. | `fontSize` | `Number` | 10 | font size in pixels -| `fontStyle` | `String` | `'normal'` | Font style to use when rendering point labels. \ No newline at end of file +| `fontStyle` | `String` | `'normal'` | Font style to use when rendering point labels. diff --git a/docs/axes/styling.md b/docs/axes/styling.md index 79cb4e9d66f..f60afd9bb68 100644 --- a/docs/axes/styling.md +++ b/docs/axes/styling.md @@ -9,10 +9,10 @@ The grid line configuration is nested under the scale configuration in the `grid | Name | Type | Default | Description | -----| ---- | --------| ----------- | `display` | `Boolean` | `true` | If false, do not display grid lines for this axis. -| `color` | Color or Color[] | `'rgba(0, 0, 0, 0.1)'` | The color of the grid lines. If specified as an array, the first color applies to the first grid line, the second to the second grid line and so on. +| `color` | `Color/Color[]` | `'rgba(0, 0, 0, 0.1)'` | The color of the grid lines. If specified as an array, the first color applies to the first grid line, the second to the second grid line and so on. | `borderDash` | `Number[]` | `[]` | Length and spacing of dashes on grid lines. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash) | `borderDashOffset` | `Number` | `0` | Offset for line dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset) -| `lineWidth` | `Number or Number[]` | `1` | Stroke width of grid lines. +| `lineWidth` | `Number/Number[]` | `1` | Stroke width of grid lines. | `drawBorder` | `Boolean` | `true` | If true, draw border at the edge between the axis and the chart area. | `drawOnChartArea` | `Boolean` | `true` | If true, draw lines on the chart area inside the axis lines. This is useful when there are multiple axes and you need to control which grid lines are drawn. | `drawTicks` | `Boolean` | `true` | If true, draw lines beside the ticks in the axis area beside the chart. @@ -30,13 +30,13 @@ The tick configuration is nested under the scale configuration in the `ticks` ke | -----| ---- | --------| ----------- | `callback` | `Function` | | Returns the string representation of the tick value as it should be displayed on the chart. See [callback](../axes/labelling.md#creating-custom-tick-formats). | `display` | `Boolean` | `true` | If true, show tick marks -| `fontColor` | Color | `'#666'` | Font color for tick labels. +| `fontColor` | `Color` | `'#666'` | Font color for tick labels. | `fontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | Font family for the tick labels, follows CSS font-family options. | `fontSize` | `Number` | `12` | Font size for the tick labels. | `fontStyle` | `String` | `'normal'` | Font style for the tick labels, follows CSS font-style options (i.e. normal, italic, oblique, initial, inherit). | `reverse` | `Boolean` | `false` | Reverses order of tick labels. -| `minor` | `object` | `{}` | Minor ticks configuration. Ommited options are inherited from options above. -| `major` | `object` | `{}` | Major ticks configuration. Ommited options are inherited from options above. +| `minor` | `object` | `{}` | Minor ticks configuration. Omitted options are inherited from options above. +| `major` | `object` | `{}` | Major ticks configuration. Omitted options are inherited from options above. ## Minor Tick Configuration The minorTick configuration is nested under the ticks configuration in the `minor` key. It defines options for the minor tick marks that are generated by the axis. Omitted options are inherited from `ticks` configuration. @@ -44,7 +44,7 @@ The minorTick configuration is nested under the ticks configuration in the `mino | Name | Type | Default | Description | -----| ---- | --------| ----------- | `callback` | `Function` | | Returns the string representation of the tick value as it should be displayed on the chart. See [callback](../axes/labelling.md#creating-custom-tick-formats). -| `fontColor` | Color | `'#666'` | Font color for tick labels. +| `fontColor` | `Color` | `'#666'` | Font color for tick labels. | `fontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | Font family for the tick labels, follows CSS font-family options. | `fontSize` | `Number` | `12` | Font size for the tick labels. | `fontStyle` | `String` | `'normal'` | Font style for the tick labels, follows CSS font-style options (i.e. normal, italic, oblique, initial, inherit). @@ -55,7 +55,7 @@ The majorTick configuration is nested under the ticks configuration in the `majo | Name | Type | Default | Description | -----| ---- | --------| ----------- | `callback` | `Function` | | Returns the string representation of the tick value as it should be displayed on the chart. See [callback](../axes/labelling.md#creating-custom-tick-formats). -| `fontColor` | Color | `'#666'` | Font color for tick labels. +| `fontColor` | `Color` | `'#666'` | Font color for tick labels. | `fontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | Font family for the tick labels, follows CSS font-family options. | `fontSize` | `Number` | `12` | Font size for the tick labels. | `fontStyle` | `String` | `'normal'` | Font style for the tick labels, follows CSS font-style options (i.e. normal, italic, oblique, initial, inherit). diff --git a/docs/charts/bar.md b/docs/charts/bar.md index 8698c644bbc..c449bf573b2 100644 --- a/docs/charts/bar.md +++ b/docs/charts/bar.md @@ -153,7 +153,7 @@ The `data` property of a dataset for a bar chart is specified as a an array of n data: [20, 10] ``` -You can also specify the dataset as x/y coordinates. +You can also specify the dataset as x/y coordinates when using the [time scale](../axes/cartesian/time.md#time-cartesian-axis). ```javascript data: [{x:'2016-12-25', y:20}, {x:'2016-12-26', y:10}] diff --git a/docs/charts/line.md b/docs/charts/line.md index 2d85ac61ab2..b4289b1619f 100644 --- a/docs/charts/line.md +++ b/docs/charts/line.md @@ -50,7 +50,7 @@ All point* properties can be specified as an array. If these are set to an array | `yAxisID` | `String` | The ID of the y axis to plot this dataset on. If not specified, this defaults to the ID of the first found y axis. | `backgroundColor` | `Color` | The fill color under the line. See [Colors](../general/colors.md#colors) | `borderColor` | `Color` | The color of the line. See [Colors](../general/colors.md#colors) -| `borderWidth` | `Number/` | The width of the line in pixels. +| `borderWidth` | `Number` | The width of the line in pixels. | `borderDash` | `Number[]` | Length and spacing of dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash) | `borderDashOffset` | `Number` | Offset for line dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset) | `borderCapStyle` | `String` | Cap style of the line. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap) @@ -120,7 +120,7 @@ The `data` property of a dataset for a line chart can be passed in two formats. data: [20, 10] ``` -When the `data` array is an array of numbers, the x axis is generally a [category](../axes/cartesian/category.md#Category Axis). The points are placed onto the axis using their position in the array. When a line chart is created with a category axis, the `labels` property of the data object must be specified. +When the `data` array is an array of numbers, the x axis is generally a [category](../axes/cartesian/category.md#category-cartesian-axis). The points are placed onto the axis using their position in the array. When a line chart is created with a category axis, the `labels` property of the data object must be specified. ### Point[] diff --git a/docs/configuration/legend.md b/docs/configuration/legend.md index 4fba028791d..4f0169e938f 100644 --- a/docs/configuration/legend.md +++ b/docs/configuration/legend.md @@ -31,7 +31,7 @@ The legend label configuration is nested below the legend configuration using th | `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 +| `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. @@ -171,6 +171,7 @@ 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 diff --git a/docs/configuration/title.md b/docs/configuration/title.md index b2c04cd8fbe..e206dfe0c55 100644 --- a/docs/configuration/title.md +++ b/docs/configuration/title.md @@ -11,11 +11,11 @@ The title configuration is passed into the `options.title` namespace. The global | `position` | `String` | `'top'` | Position of title. [more...](#position) | `fontSize` | `Number` | `12` | Font size | `fontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | Font family for the title text. -| `fontColor` | Color | `'#666'` | Font color +| `fontColor` | `Color` | `'#666'` | Font color | `fontStyle` | `String` | `'bold'` | Font style | `padding` | `Number` | `10` | Number of pixels to add above and below the title text. -| `lineHeight` | `Number|String` | `1.2` | Height of an individual line of text (see [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/line-height)) -| `text` | `String/String[]` | `''` | Title text to display. If specified as an array, text is rendered on multiple lines. +| `lineHeight` | `Number/String` | `1.2` | Height of an individual line of text (see [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/line-height)) +| `text` | `String/String[]` | `''` | Title text to display. If specified as an array, text is rendered on multiple lines. ### Position Possible title position values are: diff --git a/docs/configuration/tooltip.md b/docs/configuration/tooltip.md index bb3b7a6aefb..1be2c26bc0e 100644 --- a/docs/configuration/tooltip.md +++ b/docs/configuration/tooltip.md @@ -14,32 +14,32 @@ The tooltip configuration is passed into the `options.tooltips` namespace. The g | `callbacks` | `Object` | | See the [callbacks section](#tooltip-callbacks) | `itemSort` | `Function` | | Sort tooltip items. [more...](#sort-callback) | `filter` | `Function` | | Filter tooltip items. [more...](#filter-callback) -| `backgroundColor` | Color | `'rgba(0,0,0,0.8)'` | Background color of the tooltip. +| `backgroundColor` | `Color` | `'rgba(0,0,0,0.8)'` | Background color of the tooltip. | `titleFontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | title font | `titleFontSize` | `Number` | `12` | Title font size | `titleFontStyle` | `String` | `'bold'` | Title font style -| `titleFontColor` | Color | `'#fff'` | Title font color +| `titleFontColor` | `Color` | `'#fff'` | Title font color | `titleSpacing` | `Number` | `2` | Spacing to add to top and bottom of each title line. | `titleMarginBottom` | `Number` | `6` | Margin to add on bottom of title section. | `bodyFontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | body line font | `bodyFontSize` | `Number` | `12` | Body font size | `bodyFontStyle` | `String` | `'normal'` | Body font style -| `bodyFontColor` | Color | `'#fff'` | Body font color +| `bodyFontColor` | `Color` | `'#fff'` | Body font color | `bodySpacing` | `Number` | `2` | Spacing to add to top and bottom of each tooltip item. | `footerFontFamily` | `String` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | footer font | `footerFontSize` | `Number` | `12` | Footer font size | `footerFontStyle` | `String` | `'bold'` | Footer font style -| `footerFontColor` | Color | `'#fff'` | Footer font color -| `footerSpacing` | `Number` | `2` | Spacing to add to top and bottom of each fotter line. +| `footerFontColor` | `Color` | `'#fff'` | Footer font color +| `footerSpacing` | `Number` | `2` | Spacing to add to top and bottom of each footer line. | `footerMarginTop` | `Number` | `6` | Margin to add before drawing the footer. | `xPadding` | `Number` | `6` | Padding to add on left and right of tooltip. | `yPadding` | `Number` | `6` | Padding to add on top and bottom of tooltip. | `caretPadding` | `Number` | `2` | Extra distance to move the end of the tooltip arrow away from the tooltip point. | `caretSize` | `Number` | `5` | Size, in px, of the tooltip arrow. | `cornerRadius` | `Number` | `6` | Radius of tooltip corner curves. -| `multiKeyBackground` | Color | `'#fff'` | Color to draw behind the colored boxes when multiple items are in the tooltip +| `multiKeyBackground` | `Color` | `'#fff'` | Color to draw behind the colored boxes when multiple items are in the tooltip | `displayColors` | `Boolean` | `true` | if true, color boxes are shown in the tooltip -| `borderColor` | Color | `'rgba(0,0,0,0)'` | Color of the border +| `borderColor` | `Color` | `'rgba(0,0,0,0)'` | Color of the border | `borderWidth` | `Number` | `0` | Size of the border ### Position Modes @@ -51,6 +51,28 @@ The tooltip configuration is passed into the `options.tooltips` namespace. The g New modes can be defined by adding functions to the Chart.Tooltip.positioners map. +Example: +```javascript +/** + * Custom positioner + * @function Chart.Tooltip.positioners.custom + * @param elements {Chart.Element[]} the tooltip elements + * @param eventPosition {Point} the position of the event in canvas coordinates + * @returns {Point} the tooltip position + */ +Chart.Tooltip.positioners.custom = function(elements, eventPosition) { + /** @type {Chart.Tooltip} */ + var tooltip = this; + + /* ... */ + + return { + x: 0, + y: 0 + }; +} +``` + ### Sort Callback Allows sorting of [tooltip items](#tooltip-item-interface). Must implement at minimum a function that can be passed to [Array.prototype.sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort). This function can also accept a third parameter that is the data object passed to the chart. @@ -243,7 +265,7 @@ The tooltip model contains parameters that can be used to render the tooltip. // Body // The body lines that need to be rendered - // Each pbject contains 3 parameters + // Each object contains 3 parameters // before: String[] // lines of text before the line with the color square // lines: String[], // lines of text to render as the main item with color square // after: String[], // lines of text to render after the main lines diff --git a/docs/developers/README.md b/docs/developers/README.md index d2b0635b095..3826230a699 100644 --- a/docs/developers/README.md +++ b/docs/developers/README.md @@ -22,7 +22,12 @@ Latest builds are available for testing at: # Browser support -Chart.js offers support for all browsers where canvas is supported. +Chart.js offers support for the following browsers: +* Chrome 50+ +* Firefox 45+ +* Internet Explorer 11 +* Edge 14+ +* Safari 9+ Browser support for the canvas element is available in all modern & major mobile browsers. [CanIUse](http://caniuse.com/#feat=canvas) diff --git a/docs/developers/axes.md b/docs/developers/axes.md index 8d120195ae9..7d6c74e32c9 100644 --- a/docs/developers/axes.md +++ b/docs/developers/axes.md @@ -78,14 +78,14 @@ To work with Chart.js, custom scale types must implement the following interface // Get the pixel (x coordinate for horizontal axis, y coordinate for vertical axis) for a given value // @param index: index into the ticks array - // @param includeOffset: if true, get the pixel halway between the given tick and the next + // @param includeOffset: if true, get the pixel halfway between the given tick and the next getPixelForTick: function(index, includeOffset) {}, // Get the pixel (x coordinate for horizontal axis, y coordinate for vertical axis) for a given value // @param value : the value to get the pixel for // @param index : index into the data array of the value // @param datasetIndex : index of the dataset the value comes from - // @param includeOffset : if true, get the pixel halway between the given tick and the next + // @param includeOffset : if true, get the pixel halfway between the given tick and the next getPixelForValue: function(value, index, datasetIndex, includeOffset) {} // Get the value for a given pixel (x coordinate for horizontal axis, y coordinate for vertical axis) diff --git a/docs/developers/updates.md b/docs/developers/updates.md index 06550ce0cec..b65757f856b 100644 --- a/docs/developers/updates.md +++ b/docs/developers/updates.md @@ -1,6 +1,6 @@ # Updating Charts -It's pretty common to want to update charts after they've been created. When the chart data is changed, Chart.js will animate to the new data values. +It's pretty common to want to update charts after they've been created. When the chart data or options are changed, Chart.js will animate to the new data values and options. ## Adding or Removing Data @@ -14,9 +14,7 @@ function addData(chart, label, data) { }); chart.update(); } -``` -```javascript function removeData(chart) { chart.data.labels.pop(); chart.data.datasets.forEach((dataset) => { @@ -26,6 +24,78 @@ function removeData(chart) { } ``` +## Updating Options + +To update the options, mutating the options property in place or passing in a new options object are supported. + +- If the options are mutated in place, other option properties would be preserved, including those calculated by Chart.js. +- If created as a new object, it would be like creating a new chart with the options - old options would be discarded. + +```javascript +function updateConfigByMutating(chart) { + chart.options.title.text = 'new title'; + chart.update(); +} + +function updateConfigAsNewObject(chart) { + chart.options = { + responsive: true, + title:{ + display:true, + text: 'Chart.js' + }, + scales: { + xAxes: [{ + display: true + }], + yAxes: [{ + display: true + }] + } + } + chart.update(); +} +``` + +Scales can be updated separately without changing other options. +To update the scales, pass in an object containing all the customization including those unchanged ones. + +Variables referencing any one from `chart.scales` would be lost after updating scales with a new `id` or the changed `type`. + +```javascript +function updateScales(chart) { + var xScale = chart.scales['x-axis-0']; + var yScale = chart.scales['y-axis-0']; + chart.options.scales = { + xAxes: [{ + id: 'newId', + display: true + }], + yAxes: [{ + display: true, + type: 'logarithmic' + }] + } + chart.update(); + // need to update the reference + xScale = chart.scales['newId']; + yScale = chart.scales['y-axis-0']; +} +``` + +You can also update a specific scale either by specifying its index or id. + +```javascript +function updateScale(chart) { + chart.options.scales.yAxes[0] = { + type: 'logarithmic' + } + chart.update(); +} +``` + +Code sample for updating options can be found in [toggle-scale-type.html](../../samples/scales/toggle-scale-type.html). + ## Preventing Animations Sometimes when a chart updates, you may not want an animation. To achieve this you can call `update` with a duration of `0`. This will render the chart synchronously and without an animation. \ No newline at end of file diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md index 9266b309938..bff52c72dd6 100644 --- a/docs/getting-started/README.md +++ b/docs/getting-started/README.md @@ -40,4 +40,4 @@ var chart = new Chart(ctx, { It's that easy to get started using Chart.js! From here you can explore the many options that can help you customise your charts with scales, tooltips, labels, colors, custom actions, and much more. -There are many examples of Chart.js that are available in the `/samples` folder of `Chart.js.zip` that is attatched to every [release](https://github.com/chartjs/Chart.js/releases). \ No newline at end of file +There are many examples of Chart.js that are available in the `/samples` folder of `Chart.js.zip` that is attached to every [release](https://github.com/chartjs/Chart.js/releases). \ No newline at end of file diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index dd8cfa4a790..613d7aa7a6c 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -3,6 +3,7 @@ Chart.js can be installed via npm or bower. It is recommended to get Chart.js th ## npm [![npm](https://img.shields.io/npm/v/chart.js.svg?style=flat-square&maxAge=600)](https://npmjs.com/package/chart.js) +[![npm](https://img.shields.io/npm/dm/chart.js.svg?style=flat-square&maxAge=600)](https://npmjs.com/package/chart.js) ```bash npm install chart.js --save @@ -16,16 +17,26 @@ bower install chart.js --save ``` ## CDN -[![cdn](https://img.shields.io/cdnjs/v/Chart.js.svg?label=cdn&style=flat-square&maxAge=600)](https://cdnjs.com/libraries/Chart.js) +### CDNJS +[![cdnjs](https://img.shields.io/cdnjs/v/Chart.js.svg?style=flat-square&maxAge=600)](https://cdnjs.com/libraries/Chart.js) -or just use these [Chart.js CDN](https://cdnjs.com/libraries/Chart.js) links. +Chart.js built files are available on [CDNJS](https://cdnjs.com/): + +https://cdnjs.com/libraries/Chart.js + +### jsDelivr +[![jsdelivr](https://img.shields.io/npm/v/chart.js.svg?label=jsdelivr&style=flat-square&maxAge=600)](https://cdn.jsdelivr.net/npm/chart.js@latest/dist/) [![jsdelivr hits](https://data.jsdelivr.com/v1/package/npm/chart.js/badge)](https://www.jsdelivr.com/package/npm/chart.js) + +Chart.js built files are also available through [jsDelivr](http://www.jsdelivr.com/): + +https://www.jsdelivr.com/package/npm/chart.js?path=dist ## Github [![github](https://img.shields.io/github/release/chartjs/Chart.js.svg?style=flat-square&maxAge=600)](https://github.com/chartjs/Chart.js/releases/latest) You can download the latest version of [Chart.js on GitHub](https://github.com/chartjs/Chart.js/releases/latest). -If you download or clone the repository, you must [build](../developers/contributing.md#building-chartjs) Chart.js to generate the dist files. Chart.js no longer comes with prebuilt release versions, so an alternative option to downloading the repo is **strongly** advised. +If you download or clone the repository, you must [build](../developers/contributing.md#building-and-testing) Chart.js to generate the dist files. Chart.js no longer comes with prebuilt release versions, so an alternative option to downloading the repo is **strongly** advised. # Selecting the Correct Build diff --git a/docs/notes/extensions.md b/docs/notes/extensions.md index 33e0e647e59..0998641bacc 100644 --- a/docs/notes/extensions.md +++ b/docs/notes/extensions.md @@ -18,13 +18,19 @@ In addition, many charts can be found on the [npm registry](https://www.npmjs.co - chartjs-plugin-deferred - Defers initial chart update until chart scrolls into viewport. - chartjs-plugin-draggable - Makes select chart elements draggable with the mouse. - chartjs-plugin-stacked100 - Draws 100% stacked bar chart. + - chartjs-plugin-waterfall - Enables easy use of waterfall charts. - chartjs-plugin-zoom - Enables zooming and panning on charts. In addition, many plugins can be found on the [npm registry](https://www.npmjs.com/search?q=chartjs-plugin-). ## Integrations -### Angular +### Angular (v2+) + + - emn178/angular2-chartjs + - valor-software/ng2-charts + +### Angular (v1) - angular-chart.js - tc-angular-chartjs - angular-chartjs @@ -49,3 +55,6 @@ In addition, many plugins can be found on the [npm registry](https://www.npmjs.c ### Java - Chart.java + +### Ember.js + - ember-cli-chart diff --git a/gulpfile.js b/gulpfile.js index bb42ea9d3b8..09165c8b041 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -17,10 +17,17 @@ var browserify = require('browserify'); var source = require('vinyl-source-stream'); var merge = require('merge-stream'); var collapse = require('bundle-collapser/plugin'); -var argv = require('yargs').argv +var yargs = require('yargs'); var path = require('path'); +var fs = require('fs'); var package = require('./package.json'); +var argv = yargs + .option('force-output', {default: false}) + .option('silent-errors', {default: false}) + .option('verbose', {default: false}) + .argv + var srcDir = './src/'; var outDir = './dist/'; @@ -29,11 +36,15 @@ var header = "/*!\n" + " * http://chartjs.org/\n" + " * Version: {{ version }}\n" + " *\n" + - " * Copyright 2017 Nick Downie\n" + + " * Copyright " + (new Date().getFullYear()) + " Chart.js Contributors\n" + " * Released under the MIT license\n" + " * https://github.com/chartjs/Chart.js/blob/master/LICENSE.md\n" + " */\n"; +if (argv.verbose) { + util.log("Gulp running with options: " + JSON.stringify(argv, null, 2)); +} + gulp.task('bower', bowerTask); gulp.task('build', buildTask); gulp.task('package', packageTask); @@ -79,9 +90,25 @@ function bowerTask() { function buildTask() { + var errorHandler = function (err) { + if(argv.forceOutput) { + var browserError = 'console.error("Gulp: ' + err.toString() + '")'; + ['Chart', 'Chart.min', 'Chart.bundle', 'Chart.bundle.min'].forEach(function(fileName) { + fs.writeFileSync(outDir+fileName+'.js', browserError); + }); + } + if(argv.silentErrors) { + util.log(util.colors.red('[Error]'), err.toString()); + this.emit('end'); + } else { + throw err; + } + } + var bundled = browserify('./src/chart.js', { standalone: 'Chart' }) .plugin(collapse) .bundle() + .on('error', errorHandler) .pipe(source('Chart.bundle.js')) .pipe(insert.prepend(header)) .pipe(streamify(replace('{{ version }}', package.version))) @@ -96,6 +123,7 @@ function buildTask() { .ignore('moment') .plugin(collapse) .bundle() + .on('error', errorHandler) .pipe(source('Chart.js')) .pipe(insert.prepend(header)) .pipe(streamify(replace('{{ version }}', package.version))) diff --git a/karma.conf.js b/karma.conf.js index 3ef7f49bfa7..5601cbd7212 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -14,7 +14,13 @@ module.exports = function(karma) { browserify: { debug: true - } + }, + + // These settings deal with browser disconnects. We had seen test flakiness from Firefox + // [Firefox 56.0.0 (Linux 0.0.0)]: Disconnected (1 times), because no message in 10000 ms. + // https://github.com/jasmine/jasmine/issues/1327#issuecomment-332939551 + browserNoActivityTimeout: 60000, + browserDisconnectTolerance: 3 }; // https://swizec.com/blog/how-to-run-javascript-tests-in-chrome-on-travis/swizec/6647 diff --git a/package.json b/package.json index 445dc877ab0..c7d21043b1a 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "chart.js", "homepage": "http://www.chartjs.org", "description": "Simple HTML5 charts using the canvas element.", - "version": "2.7.0", + "version": "2.7.1", "license": "MIT", "main": "src/chart.js", "repository": { @@ -10,30 +10,32 @@ "url": "https://github.com/chartjs/Chart.js.git" }, "devDependencies": { - "browserify": "^14.3.0", - "browserify-istanbul": "^2.0.0", - "bundle-collapser": "^1.2.1", + "browserify": "^14.5.0", + "browserify-istanbul": "^3.0.1", + "bundle-collapser": "^1.3.0", "child-process-promise": "^2.2.1", - "coveralls": "^2.13.1", - "gitbook-cli": "^2.3.0", + "coveralls": "^3.0.0", + "eslint": "^4.9.0", + "eslint-config-chartjs": "^0.1.0", + "gitbook-cli": "^2.3.2", "gulp": "3.9.x", "gulp-concat": "~2.6.x", "gulp-connect": "~5.0.0", - "gulp-eslint": "^3.0.1", + "gulp-eslint": "^4.0.0", "gulp-file": "^0.3.0", "gulp-html-validator": "^0.0.5", "gulp-insert": "~0.5.0", - "gulp-replace": "^0.5.4", + "gulp-replace": "^0.6.1", "gulp-size": "~2.1.0", "gulp-streamify": "^1.0.2", "gulp-uglify": "~3.0.x", "gulp-util": "~3.0.x", "gulp-zip": "~4.0.0", - "jasmine": "^2.6.0", - "jasmine-core": "^2.6.2", - "karma": "^1.7.0", + "jasmine": "^2.8.0", + "jasmine-core": "^2.8.0", + "karma": "^1.7.1", "karma-browserify": "^5.1.1", - "karma-chrome-launcher": "^2.1.1", + "karma-chrome-launcher": "^2.2.0", "karma-coverage": "^1.1.1", "karma-firefox-launcher": "^1.0.1", "karma-jasmine": "^1.1.0", @@ -42,13 +44,13 @@ "pixelmatch": "^4.0.2", "vinyl-source-stream": "^1.1.0", "watchify": "^3.9.0", - "yargs": "^8.0.1" + "yargs": "^9.0.1" }, "spm": { "main": "Chart.js" }, "dependencies": { - "chartjs-color": "~2.2.0", - "moment": "~2.18.0" + "chartjs-color": "^2.1.0", + "moment": "^2.10.2" } } diff --git a/samples/samples.js b/samples/samples.js index 2d11e9d48c9..b818827c6d2 100644 --- a/samples/samples.js +++ b/samples/samples.js @@ -136,6 +136,9 @@ }, { title: 'Non numeric Y Axis', path: 'scales/non-numeric-y.html' + }, { + title: 'Toggle Scale Type', + path: 'scales/toggle-scale-type.html' }] }, { title: 'Legend', diff --git a/samples/scales/gridlines-display.html b/samples/scales/gridlines-display.html index 0c2dc114e6f..c21469329b8 100644 --- a/samples/scales/gridlines-display.html +++ b/samples/scales/gridlines-display.html @@ -2,7 +2,7 @@ - Suggested Min/Max Settings + Grid Lines Display Settings + + + +
+ +
+ + + + + diff --git a/src/chart.js b/src/chart.js index df17bb92c3f..a958e343ff8 100644 --- a/src/chart.js +++ b/src/chart.js @@ -12,13 +12,14 @@ Chart.defaults = require('./core/core.defaults'); Chart.Element = require('./core/core.element'); Chart.elements = require('./elements/index'); Chart.Interaction = require('./core/core.interaction'); +Chart.layouts = require('./core/core.layouts'); Chart.platform = require('./platforms/platform'); +Chart.plugins = require('./core/core.plugins'); +Chart.Ticks = require('./core/core.ticks'); -require('./core/core.plugin')(Chart); require('./core/core.animation')(Chart); require('./core/core.controller')(Chart); require('./core/core.datasetController')(Chart); -require('./core/core.layoutService')(Chart); require('./core/core.scaleService')(Chart); require('./core/core.scale')(Chart); require('./core/core.tooltip')(Chart); @@ -49,15 +50,12 @@ require('./charts/Chart.Radar')(Chart); require('./charts/Chart.Scatter')(Chart); // Loading built-it plugins -var plugins = []; - -plugins.push( - require('./plugins/plugin.filler')(Chart), - require('./plugins/plugin.legend')(Chart), - require('./plugins/plugin.title')(Chart) -); - -Chart.plugins.register(plugins); +var plugins = require('./plugins'); +for (var k in plugins) { + if (plugins.hasOwnProperty(k)) { + Chart.plugins.register(plugins[k]); + } +} Chart.platform.initialize(); @@ -68,6 +66,43 @@ if (typeof window !== 'undefined') { // DEPRECATIONS +/** + * Provided for backward compatibility, not available anymore + * @namespace Chart.Legend + * @deprecated since version 2.1.5 + * @todo remove at version 3 + * @private + */ +Chart.Legend = plugins.legend._element; + +/** + * Provided for backward compatibility, not available anymore + * @namespace Chart.Title + * @deprecated since version 2.1.5 + * @todo remove at version 3 + * @private + */ +Chart.Title = plugins.title._element; + +/** + * Provided for backward compatibility, use Chart.plugins instead + * @namespace Chart.pluginService + * @deprecated since version 2.1.5 + * @todo remove at version 3 + * @private + */ +Chart.pluginService = Chart.plugins; + +/** + * Provided for backward compatibility, inheriting from Chart.PlugingBase has no + * effect, instead simply create/register plugins via plain JavaScript objects. + * @interface Chart.PluginBase + * @deprecated since version 2.5.0 + * @todo remove at version 3 + * @private + */ +Chart.PluginBase = Chart.Element.extend({}); + /** * Provided for backward compatibility, use Chart.helpers.canvas instead. * @namespace Chart.canvasHelpers @@ -76,3 +111,12 @@ if (typeof window !== 'undefined') { * @private */ Chart.canvasHelpers = Chart.helpers.canvas; + +/** + * Provided for backward compatibility, use Chart.layouts instead. + * @namespace Chart.layoutService + * @deprecated since version 2.8.0 + * @todo remove at version 3 + * @private + */ +Chart.layoutService = Chart.layouts; diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index ae4aee45d17..ff2b56ae5a0 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -95,6 +95,93 @@ defaults._set('horizontalBar', { } }); +/** + * Computes the "optimal" sample size to maintain bars equally sized while preventing overlap. + * @private + */ +function computeMinSampleSize(scale, pixels) { + var min = scale.isHorizontal() ? scale.width : scale.height; + var ticks = scale.getTicks(); + var prev, curr, i, ilen; + + for (i = 1, ilen = pixels.length; i < ilen; ++i) { + min = Math.min(min, pixels[i] - pixels[i - 1]); + } + + for (i = 0, ilen = ticks.length; i < ilen; ++i) { + curr = scale.getPixelForTick(i); + min = i > 0 ? Math.min(min, curr - prev) : min; + prev = curr; + } + + return min; +} + +/** + * Computes an "ideal" category based on the absolute bar thickness or, if undefined or null, + * uses the smallest interval (see computeMinSampleSize) that prevents bar overlapping. This + * mode currently always generates bars equally sized (until we introduce scriptable options?). + * @private + */ +function computeFitCategoryTraits(index, ruler, options) { + var thickness = options.barThickness; + var count = ruler.stackCount; + var curr = ruler.pixels[index]; + var size, ratio; + + if (helpers.isNullOrUndef(thickness)) { + size = ruler.min * options.categoryPercentage; + ratio = options.barPercentage; + } else { + // When bar thickness is enforced, category and bar percentages are ignored. + // Note(SB): we could add support for relative bar thickness (e.g. barThickness: '50%') + // and deprecate barPercentage since this value is ignored when thickness is absolute. + size = thickness * count; + ratio = 1; + } + + return { + chunk: size / count, + ratio: ratio, + start: curr - (size / 2) + }; +} + +/** + * Computes an "optimal" category that globally arranges bars side by side (no gap when + * percentage options are 1), based on the previous and following categories. This mode + * generates bars with different widths when data are not evenly spaced. + * @private + */ +function computeFlexCategoryTraits(index, ruler, options) { + var pixels = ruler.pixels; + var curr = pixels[index]; + var prev = index > 0 ? pixels[index - 1] : null; + var next = index < pixels.length - 1 ? pixels[index + 1] : null; + var percent = options.categoryPercentage; + var start, size; + + if (prev === null) { + // first data: its size is double based on the next point or, + // if it's also the last data, we use the scale end extremity. + prev = curr - (next === null ? ruler.end - curr : next - curr); + } + + if (next === null) { + // last data: its size is also double based on the previous point. + next = curr + curr - prev; + } + + start = curr - ((curr - prev) / 2) * percent; + size = ((next - prev) / 2) * percent; + + return { + chunk: size / ruler.stackCount, + ratio: options.barPercentage, + start: start + }; +} + module.exports = function(Chart) { Chart.controllers.bar = Chart.DatasetController.extend({ @@ -201,10 +288,12 @@ module.exports = function(Chart) { }, /** - * Returns the effective number of stacks based on groups and bar visibility. + * Returns the stacks based on groups and bar visibility. + * @param {Number} [last] - The dataset index + * @returns {Array} The stack list * @private */ - getStackCount: function(last) { + _getStacks: function(last) { var me = this; var chart = me.chart; var scale = me.getIndexScale(); @@ -223,15 +312,33 @@ module.exports = function(Chart) { } } - return stacks.length; + return stacks; + }, + + /** + * Returns the effective number of stacks based on groups and bar visibility. + * @private + */ + getStackCount: function() { + return this._getStacks().length; }, /** * Returns the stack index for the given dataset based on groups and bar visibility. + * @param {Number} [datasetIndex] - The dataset index + * @param {String} [name] - The stack name to find + * @returns {Number} The stack index * @private */ - getStackIndex: function(datasetIndex) { - return this.getStackCount(datasetIndex) - 1; + getStackIndex: function(datasetIndex, name) { + var stacks = this._getStacks(datasetIndex); + var index = (name !== undefined) + ? stacks.indexOf(name) + : -1; // indexOf returns -1 if element is not present + + return (index === -1) + ? stacks.length - 1 + : index; }, /** @@ -242,17 +349,22 @@ module.exports = function(Chart) { var scale = me.getIndexScale(); var stackCount = me.getStackCount(); var datasetIndex = me.index; - var pixels = []; var isHorizontal = scale.isHorizontal(); var start = isHorizontal ? scale.left : scale.top; var end = start + (isHorizontal ? scale.width : scale.height); - var i, ilen; + var pixels = []; + var i, ilen, min; for (i = 0, ilen = me.getMeta().data.length; i < ilen; ++i) { pixels.push(scale.getPixelForValue(null, i, datasetIndex)); } + min = helpers.isNullOrUndef(scale.options.barThickness) + ? computeMinSampleSize(scale, pixels) + : -1; + return { + min: min, pixels: pixels, start: start, end: end, @@ -312,50 +424,21 @@ module.exports = function(Chart) { calculateBarIndexPixels: function(datasetIndex, index, ruler) { var me = this; var options = ruler.scale.options; - var stackIndex = me.getStackIndex(datasetIndex); - var pixels = ruler.pixels; - var base = pixels[index]; - var length = pixels.length; - var start = ruler.start; - var end = ruler.end; - var leftSampleSize, rightSampleSize, leftCategorySize, rightCategorySize, fullBarSize, size; - - if (length === 1) { - leftSampleSize = base > start ? base - start : end - base; - rightSampleSize = base < end ? end - base : base - start; - } else { - if (index > 0) { - leftSampleSize = (base - pixels[index - 1]) / 2; - if (index === length - 1) { - rightSampleSize = leftSampleSize; - } - } - if (index < length - 1) { - rightSampleSize = (pixels[index + 1] - base) / 2; - if (index === 0) { - leftSampleSize = rightSampleSize; - } - } - } + var range = options.barThickness === 'flex' + ? computeFlexCategoryTraits(index, ruler, options) + : computeFitCategoryTraits(index, ruler, options); - leftCategorySize = leftSampleSize * options.categoryPercentage; - rightCategorySize = rightSampleSize * options.categoryPercentage; - fullBarSize = (leftCategorySize + rightCategorySize) / ruler.stackCount; - size = fullBarSize * options.barPercentage; - - size = Math.min( - helpers.valueOrDefault(options.barThickness, size), - helpers.valueOrDefault(options.maxBarThickness, Infinity)); - - base -= leftCategorySize; - base += fullBarSize * stackIndex; - base += (fullBarSize - size) / 2; + var stackIndex = me.getStackIndex(datasetIndex, me.getMeta().stack); + var center = range.start + (range.chunk * stackIndex) + (range.chunk / 2); + var size = Math.min( + helpers.valueOrDefault(options.maxBarThickness, Infinity), + range.chunk * range.ratio); return { - size: size, - base: base, - head: base + size, - center: base + size / 2 + base: center - size / 2, + head: center + size / 2, + center: center, + size: size }; }, diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 03dfedb7196..e29a5b0769c 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -3,10 +3,11 @@ var defaults = require('./core.defaults'); var helpers = require('../helpers/index'); var Interaction = require('./core.interaction'); +var layouts = require('./core.layouts'); var platform = require('../platforms/platform'); +var plugins = require('./core.plugins'); module.exports = function(Chart) { - var plugins = Chart.plugins; // Create a dictionary of chart types, to allow for extension of existing types Chart.types = {}; @@ -45,17 +46,21 @@ module.exports = function(Chart) { function updateConfig(chart) { var newOptions = chart.options; - // Update Scale(s) with options - if (newOptions.scale) { - chart.scale.options = newOptions.scale; - } else if (newOptions.scales) { - newOptions.scales.xAxes.concat(newOptions.scales.yAxes).forEach(function(scaleOptions) { - chart.scales[scaleOptions.id].options = scaleOptions; - }); - } + helpers.each(chart.scales, function(scale) { + layouts.removeBox(chart, scale); + }); + + newOptions = helpers.configMerge( + Chart.defaults.global, + Chart.defaults[chart.config.type], + newOptions); + chart.options = chart.config.options = newOptions; + chart.ensureScalesHaveIDs(); + chart.buildOrUpdateScales(); // Tooltip chart.tooltip._options = newOptions.tooltips; + chart.tooltip.initialize(); } function positionIsHorizontal(position) { @@ -143,7 +148,7 @@ module.exports = function(Chart) { // Make sure scales have IDs and are built before we build any controllers. me.ensureScalesHaveIDs(); - me.buildScales(); + me.buildOrUpdateScales(); me.initToolTip(); // After init plugin notification @@ -223,11 +228,15 @@ module.exports = function(Chart) { /** * Builds a map of scale ID to scale object for future lookup. */ - buildScales: function() { + buildOrUpdateScales: function() { var me = this; var options = me.options; - var scales = me.scales = {}; + var scales = me.scales || {}; var items = []; + var updated = Object.keys(scales).reduce(function(obj, id) { + obj[id] = false; + return obj; + }, {}); if (options.scales) { items = items.concat( @@ -251,24 +260,35 @@ module.exports = function(Chart) { helpers.each(items, function(item) { var scaleOptions = item.options; + var id = scaleOptions.id; var scaleType = helpers.valueOrDefault(scaleOptions.type, item.dtype); - var scaleClass = Chart.scaleService.getScaleConstructor(scaleType); - if (!scaleClass) { - return; - } if (positionIsHorizontal(scaleOptions.position) !== positionIsHorizontal(item.dposition)) { scaleOptions.position = item.dposition; } - var scale = new scaleClass({ - id: scaleOptions.id, - options: scaleOptions, - ctx: me.ctx, - chart: me - }); + updated[id] = true; + var scale = null; + if (id in scales && scales[id].type === scaleType) { + scale = scales[id]; + scale.options = scaleOptions; + scale.ctx = me.ctx; + scale.chart = me; + } else { + var scaleClass = Chart.scaleService.getScaleConstructor(scaleType); + if (!scaleClass) { + return; + } + scale = new scaleClass({ + id: id, + type: scaleType, + options: scaleOptions, + ctx: me.ctx, + chart: me + }); + scales[scale.id] = scale; + } - scales[scale.id] = scale; scale.mergeTicksOptions(); // TODO(SB): I think we should be able to remove this custom case (options.scale) @@ -278,6 +298,14 @@ module.exports = function(Chart) { me.scale = scale; } }); + // clear up discarded scales + helpers.each(updated, function(hasUpdated, id) { + if (!hasUpdated) { + delete scales[id]; + } + }); + + me.scales = scales; Chart.scaleService.addScalesToLayout(this); }, @@ -301,6 +329,7 @@ module.exports = function(Chart) { if (meta.controller) { meta.controller.updateIndex(datasetIndex); + meta.controller.linkScales(); } else { var ControllerClass = Chart.controllers[meta.type]; if (ControllerClass === undefined) { @@ -347,6 +376,10 @@ module.exports = function(Chart) { updateConfig(me); + // plugins options references might have change, let's invalidate the cache + // https://github.com/chartjs/Chart.js/issues/5111#issuecomment-355934167 + plugins._invalidate(me); + if (plugins.notify(me, 'beforeUpdate') === false) { return; } @@ -365,12 +398,22 @@ module.exports = function(Chart) { me.updateLayout(); // Can only reset the new controllers after the scales have been updated - helpers.each(newControllers, function(controller) { - controller.reset(); - }); + if (me.options.animation && me.options.animation.duration) { + helpers.each(newControllers, function(controller) { + controller.reset(); + }); + } me.updateDatasets(); + // Need to reset tooltip in case it is displayed with elements that are removed + // after update. + me.tooltip.initialize(); + + // Last active contains items that were previously in the tooltip. + // When we reset the tooltip, we need to clear it + me.lastActive = []; + // Do this before render so that any plugins that need final scale updates can use it plugins.notify(me, 'afterUpdate'); @@ -397,7 +440,7 @@ module.exports = function(Chart) { return; } - Chart.layoutService.update(this, this.width, this.height); + layouts.update(this, this.width, this.height); /** * Provided for backward compatibility, use `afterLayout` instead. @@ -528,9 +571,7 @@ module.exports = function(Chart) { } me.drawDatasets(easingValue); - - // Finally draw the tooltip - me.tooltip.draw(); + me._drawTooltip(easingValue); plugins.notify(me, 'afterDraw', [easingValue]); }, @@ -595,6 +636,28 @@ module.exports = function(Chart) { plugins.notify(me, 'afterDatasetDraw', [args]); }, + /** + * Draws tooltip unless a plugin returns `false` to the `beforeTooltipDraw` + * hook, in which case, plugins will not be called on `afterTooltipDraw`. + * @private + */ + _drawTooltip: function(easingValue) { + var me = this; + var tooltip = me.tooltip; + var args = { + tooltip: tooltip, + easingValue: easingValue + }; + + if (plugins.notify(me, 'beforeTooltipDraw', [args]) === false) { + return; + } + + tooltip.draw(); + + plugins.notify(me, 'afterTooltipDraw', [args]); + }, + // Get the single element that was clicked on // @return : An object containing the dataset index and element index of the matching element. Also contains the rectangle that was draw getElementAtEvent: function(e) { @@ -791,7 +854,15 @@ module.exports = function(Chart) { me._bufferedRequest = null; var changed = me.handleEvent(e); - changed |= tooltip && tooltip.handleEvent(e); + // for smooth tooltip animations issue #4989 + // the tooltip should be the source of change + // Animation check workaround: + // tooltip._start will be null when tooltip isn't animating + if (tooltip) { + changed = tooltip._start + ? tooltip.handleEvent(e) + : changed | tooltip.handleEvent(e); + } plugins.notify(me, 'afterEvent', [e]); diff --git a/src/core/core.datasetController.js b/src/core/core.datasetController.js index 67dbe27f200..ee6158c35b1 100644 --- a/src/core/core.datasetController.js +++ b/src/core/core.datasetController.js @@ -111,10 +111,10 @@ module.exports = function(Chart) { var meta = me.getMeta(); var dataset = me.getDataset(); - if (meta.xAxisID === null) { + if (meta.xAxisID === null || !(meta.xAxisID in me.chart.scales)) { meta.xAxisID = dataset.xAxisID || me.chart.options.scales.xAxes[0].id; } - if (meta.yAxisID === null) { + if (meta.yAxisID === null || !(meta.yAxisID in me.chart.scales)) { meta.yAxisID = dataset.yAxisID || me.chart.options.scales.yAxes[0].id; } }, diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index 0b9cea52eb3..bdce895cf70 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -10,16 +10,6 @@ module.exports = function(Chart) { // -- Basic js utility methods - helpers.extend = function(base) { - var setFn = function(value, key) { - base[key] = value; - }; - for (var i = 1, ilen = arguments.length; i < ilen; i++) { - helpers.each(arguments[i], setFn); - } - return base; - }; - helpers.configMerge = function(/* objects ... */) { return helpers.merge(helpers.clone(arguments[0]), [].slice.call(arguments, 1), { merger: function(key, target, source, options) { @@ -125,29 +115,7 @@ module.exports = function(Chart) { } } }; - helpers.inherits = function(extensions) { - // Basic javascript inheritance based on the model created in Backbone.js - var me = this; - var ChartElement = (extensions && extensions.hasOwnProperty('constructor')) ? extensions.constructor : function() { - return me.apply(this, arguments); - }; - - var Surrogate = function() { - this.constructor = ChartElement; - }; - Surrogate.prototype = me.prototype; - ChartElement.prototype = new Surrogate(); - ChartElement.extend = helpers.inherits; - - if (extensions) { - helpers.extend(ChartElement.prototype, extensions); - } - - ChartElement.__super__ = me.prototype; - - return ChartElement; - }; // -- Math methods helpers.isNumber = function(n) { return !isNaN(parseFloat(n)) && isFinite(n); @@ -544,8 +512,10 @@ module.exports = function(Chart) { // If no style has been set on the canvas, the render size is used as display size, // making the chart visually bigger, so let's enforce it to the "correct" values. // See https://github.com/chartjs/Chart.js/issues/3575 - canvas.style.height = height + 'px'; - canvas.style.width = width + 'px'; + if (!canvas.style.height && !canvas.style.width) { + canvas.style.height = height + 'px'; + canvas.style.width = width + 'px'; + } }; // -- Canvas methods helpers.fontString = function(pixelSize, fontStyle, fontFamily) { diff --git a/src/core/core.layoutService.js b/src/core/core.layoutService.js deleted file mode 100644 index 5649f113dc2..00000000000 --- a/src/core/core.layoutService.js +++ /dev/null @@ -1,422 +0,0 @@ -'use strict'; - -var helpers = require('../helpers/index'); - -module.exports = function(Chart) { - - function filterByPosition(array, position) { - return helpers.where(array, function(v) { - return v.position === position; - }); - } - - function sortByWeight(array, reverse) { - array.forEach(function(v, i) { - v._tmpIndex_ = i; - return v; - }); - array.sort(function(a, b) { - var v0 = reverse ? b : a; - var v1 = reverse ? a : b; - return v0.weight === v1.weight ? - v0._tmpIndex_ - v1._tmpIndex_ : - v0.weight - v1.weight; - }); - array.forEach(function(v) { - delete v._tmpIndex_; - }); - } - - /** - * @interface ILayoutItem - * @prop {String} position - The position of the item in the chart layout. Possible values are - * 'left', 'top', 'right', 'bottom', and 'chartArea' - * @prop {Number} weight - The weight used to sort the item. Higher weights are further away from the chart area - * @prop {Boolean} fullWidth - if true, and the item is horizontal, then push vertical boxes down - * @prop {Function} isHorizontal - returns true if the layout item is horizontal (ie. top or bottom) - * @prop {Function} update - Takes two parameters: width and height. Returns size of item - * @prop {Function} getPadding - Returns an object with padding on the edges - * @prop {Number} width - Width of item. Must be valid after update() - * @prop {Number} height - Height of item. Must be valid after update() - * @prop {Number} left - Left edge of the item. Set by layout system and cannot be used in update - * @prop {Number} top - Top edge of the item. Set by layout system and cannot be used in update - * @prop {Number} right - Right edge of the item. Set by layout system and cannot be used in update - * @prop {Number} bottom - Bottom edge of the item. Set by layout system and cannot be used in update - */ - - // The layout service is very self explanatory. It's responsible for the layout within a chart. - // Scales, Legends and Plugins all rely on the layout service and can easily register to be placed anywhere they need - // It is this service's responsibility of carrying out that layout. - Chart.layoutService = { - defaults: {}, - - /** - * Register a box to a chart. - * A box is simply a reference to an object that requires layout. eg. Scales, Legend, Title. - * @param {Chart} chart - the chart to use - * @param {ILayoutItem} item - the item to add to be layed out - */ - addBox: function(chart, item) { - if (!chart.boxes) { - chart.boxes = []; - } - - // initialize item with default values - item.fullWidth = item.fullWidth || false; - item.position = item.position || 'top'; - item.weight = item.weight || 0; - - chart.boxes.push(item); - }, - - /** - * Remove a layoutItem from a chart - * @param {Chart} chart - the chart to remove the box from - * @param {Object} layoutItem - the item to remove from the layout - */ - removeBox: function(chart, layoutItem) { - var index = chart.boxes ? chart.boxes.indexOf(layoutItem) : -1; - if (index !== -1) { - chart.boxes.splice(index, 1); - } - }, - - /** - * Sets (or updates) options on the given `item`. - * @param {Chart} chart - the chart in which the item lives (or will be added to) - * @param {Object} item - the item to configure with the given options - * @param {Object} options - the new item options. - */ - configure: function(chart, item, options) { - var props = ['fullWidth', 'position', 'weight']; - var ilen = props.length; - var i = 0; - var prop; - - for (; i < ilen; ++i) { - prop = props[i]; - if (options.hasOwnProperty(prop)) { - item[prop] = options[prop]; - } - } - }, - - /** - * Fits boxes of the given chart into the given size by having each box measure itself - * then running a fitting algorithm - * @param {Chart} chart - the chart - * @param {Number} width - the width to fit into - * @param {Number} height - the height to fit into - */ - update: function(chart, width, height) { - if (!chart) { - return; - } - - var layoutOptions = chart.options.layout || {}; - var padding = helpers.options.toPadding(layoutOptions.padding); - var leftPadding = padding.left; - var rightPadding = padding.right; - var topPadding = padding.top; - var bottomPadding = padding.bottom; - - var leftBoxes = filterByPosition(chart.boxes, 'left'); - var rightBoxes = filterByPosition(chart.boxes, 'right'); - var topBoxes = filterByPosition(chart.boxes, 'top'); - var bottomBoxes = filterByPosition(chart.boxes, 'bottom'); - var chartAreaBoxes = filterByPosition(chart.boxes, 'chartArea'); - - // Sort boxes by weight. A higher weight is further away from the chart area - sortByWeight(leftBoxes, true); - sortByWeight(rightBoxes, false); - sortByWeight(topBoxes, true); - sortByWeight(bottomBoxes, false); - - // Essentially we now have any number of boxes on each of the 4 sides. - // Our canvas looks like the following. - // The areas L1 and L2 are the left axes. R1 is the right axis, T1 is the top axis and - // B1 is the bottom axis - // There are also 4 quadrant-like locations (left to right instead of clockwise) reserved for chart overlays - // These locations are single-box locations only, when trying to register a chartArea location that is already taken, - // an error will be thrown. - // - // |----------------------------------------------------| - // | T1 (Full Width) | - // |----------------------------------------------------| - // | | | T2 | | - // | |----|-------------------------------------|----| - // | | | C1 | | C2 | | - // | | |----| |----| | - // | | | | | - // | L1 | L2 | ChartArea (C0) | R1 | - // | | | | | - // | | |----| |----| | - // | | | C3 | | C4 | | - // | |----|-------------------------------------|----| - // | | | B1 | | - // |----------------------------------------------------| - // | B2 (Full Width) | - // |----------------------------------------------------| - // - // What we do to find the best sizing, we do the following - // 1. Determine the minimum size of the chart area. - // 2. Split the remaining width equally between each vertical axis - // 3. Split the remaining height equally between each horizontal axis - // 4. Give each layout the maximum size it can be. The layout will return it's minimum size - // 5. Adjust the sizes of each axis based on it's minimum reported size. - // 6. Refit each axis - // 7. Position each axis in the final location - // 8. Tell the chart the final location of the chart area - // 9. Tell any axes that overlay the chart area the positions of the chart area - - // Step 1 - var chartWidth = width - leftPadding - rightPadding; - var chartHeight = height - topPadding - bottomPadding; - var chartAreaWidth = chartWidth / 2; // min 50% - var chartAreaHeight = chartHeight / 2; // min 50% - - // Step 2 - var verticalBoxWidth = (width - chartAreaWidth) / (leftBoxes.length + rightBoxes.length); - - // Step 3 - var horizontalBoxHeight = (height - chartAreaHeight) / (topBoxes.length + bottomBoxes.length); - - // Step 4 - var maxChartAreaWidth = chartWidth; - var maxChartAreaHeight = chartHeight; - var minBoxSizes = []; - - function getMinimumBoxSize(box) { - var minSize; - var isHorizontal = box.isHorizontal(); - - if (isHorizontal) { - minSize = box.update(box.fullWidth ? chartWidth : maxChartAreaWidth, horizontalBoxHeight); - maxChartAreaHeight -= minSize.height; - } else { - minSize = box.update(verticalBoxWidth, chartAreaHeight); - maxChartAreaWidth -= minSize.width; - } - - minBoxSizes.push({ - horizontal: isHorizontal, - minSize: minSize, - box: box, - }); - } - - helpers.each(leftBoxes.concat(rightBoxes, topBoxes, bottomBoxes), getMinimumBoxSize); - - // If a horizontal box has padding, we move the left boxes over to avoid ugly charts (see issue #2478) - var maxHorizontalLeftPadding = 0; - var maxHorizontalRightPadding = 0; - var maxVerticalTopPadding = 0; - var maxVerticalBottomPadding = 0; - - helpers.each(topBoxes.concat(bottomBoxes), function(horizontalBox) { - if (horizontalBox.getPadding) { - var boxPadding = horizontalBox.getPadding(); - maxHorizontalLeftPadding = Math.max(maxHorizontalLeftPadding, boxPadding.left); - maxHorizontalRightPadding = Math.max(maxHorizontalRightPadding, boxPadding.right); - } - }); - - helpers.each(leftBoxes.concat(rightBoxes), function(verticalBox) { - if (verticalBox.getPadding) { - var boxPadding = verticalBox.getPadding(); - maxVerticalTopPadding = Math.max(maxVerticalTopPadding, boxPadding.top); - maxVerticalBottomPadding = Math.max(maxVerticalBottomPadding, boxPadding.bottom); - } - }); - - // At this point, maxChartAreaHeight and maxChartAreaWidth are the size the chart area could - // be if the axes are drawn at their minimum sizes. - // Steps 5 & 6 - var totalLeftBoxesWidth = leftPadding; - var totalRightBoxesWidth = rightPadding; - var totalTopBoxesHeight = topPadding; - var totalBottomBoxesHeight = bottomPadding; - - // Function to fit a box - function fitBox(box) { - var minBoxSize = helpers.findNextWhere(minBoxSizes, function(minBox) { - return minBox.box === box; - }); - - if (minBoxSize) { - if (box.isHorizontal()) { - var scaleMargin = { - left: Math.max(totalLeftBoxesWidth, maxHorizontalLeftPadding), - right: Math.max(totalRightBoxesWidth, maxHorizontalRightPadding), - top: 0, - bottom: 0 - }; - - // Don't use min size here because of label rotation. When the labels are rotated, their rotation highly depends - // on the margin. Sometimes they need to increase in size slightly - box.update(box.fullWidth ? chartWidth : maxChartAreaWidth, chartHeight / 2, scaleMargin); - } else { - box.update(minBoxSize.minSize.width, maxChartAreaHeight); - } - } - } - - // Update, and calculate the left and right margins for the horizontal boxes - helpers.each(leftBoxes.concat(rightBoxes), fitBox); - - helpers.each(leftBoxes, function(box) { - totalLeftBoxesWidth += box.width; - }); - - helpers.each(rightBoxes, function(box) { - totalRightBoxesWidth += box.width; - }); - - // Set the Left and Right margins for the horizontal boxes - helpers.each(topBoxes.concat(bottomBoxes), fitBox); - - // Figure out how much margin is on the top and bottom of the vertical boxes - helpers.each(topBoxes, function(box) { - totalTopBoxesHeight += box.height; - }); - - helpers.each(bottomBoxes, function(box) { - totalBottomBoxesHeight += box.height; - }); - - function finalFitVerticalBox(box) { - var minBoxSize = helpers.findNextWhere(minBoxSizes, function(minSize) { - return minSize.box === box; - }); - - var scaleMargin = { - left: 0, - right: 0, - top: totalTopBoxesHeight, - bottom: totalBottomBoxesHeight - }; - - if (minBoxSize) { - box.update(minBoxSize.minSize.width, maxChartAreaHeight, scaleMargin); - } - } - - // Let the left layout know the final margin - helpers.each(leftBoxes.concat(rightBoxes), finalFitVerticalBox); - - // Recalculate because the size of each layout might have changed slightly due to the margins (label rotation for instance) - totalLeftBoxesWidth = leftPadding; - totalRightBoxesWidth = rightPadding; - totalTopBoxesHeight = topPadding; - totalBottomBoxesHeight = bottomPadding; - - helpers.each(leftBoxes, function(box) { - totalLeftBoxesWidth += box.width; - }); - - helpers.each(rightBoxes, function(box) { - totalRightBoxesWidth += box.width; - }); - - helpers.each(topBoxes, function(box) { - totalTopBoxesHeight += box.height; - }); - helpers.each(bottomBoxes, function(box) { - totalBottomBoxesHeight += box.height; - }); - - // We may be adding some padding to account for rotated x axis labels - var leftPaddingAddition = Math.max(maxHorizontalLeftPadding - totalLeftBoxesWidth, 0); - totalLeftBoxesWidth += leftPaddingAddition; - totalRightBoxesWidth += Math.max(maxHorizontalRightPadding - totalRightBoxesWidth, 0); - - var topPaddingAddition = Math.max(maxVerticalTopPadding - totalTopBoxesHeight, 0); - totalTopBoxesHeight += topPaddingAddition; - totalBottomBoxesHeight += Math.max(maxVerticalBottomPadding - totalBottomBoxesHeight, 0); - - // Figure out if our chart area changed. This would occur if the dataset layout label rotation - // changed due to the application of the margins in step 6. Since we can only get bigger, this is safe to do - // without calling `fit` again - var newMaxChartAreaHeight = height - totalTopBoxesHeight - totalBottomBoxesHeight; - var newMaxChartAreaWidth = width - totalLeftBoxesWidth - totalRightBoxesWidth; - - if (newMaxChartAreaWidth !== maxChartAreaWidth || newMaxChartAreaHeight !== maxChartAreaHeight) { - helpers.each(leftBoxes, function(box) { - box.height = newMaxChartAreaHeight; - }); - - helpers.each(rightBoxes, function(box) { - box.height = newMaxChartAreaHeight; - }); - - helpers.each(topBoxes, function(box) { - if (!box.fullWidth) { - box.width = newMaxChartAreaWidth; - } - }); - - helpers.each(bottomBoxes, function(box) { - if (!box.fullWidth) { - box.width = newMaxChartAreaWidth; - } - }); - - maxChartAreaHeight = newMaxChartAreaHeight; - maxChartAreaWidth = newMaxChartAreaWidth; - } - - // Step 7 - Position the boxes - var left = leftPadding + leftPaddingAddition; - var top = topPadding + topPaddingAddition; - - function placeBox(box) { - if (box.isHorizontal()) { - box.left = box.fullWidth ? leftPadding : totalLeftBoxesWidth; - box.right = box.fullWidth ? width - rightPadding : totalLeftBoxesWidth + maxChartAreaWidth; - box.top = top; - box.bottom = top + box.height; - - // Move to next point - top = box.bottom; - - } else { - - box.left = left; - box.right = left + box.width; - box.top = totalTopBoxesHeight; - box.bottom = totalTopBoxesHeight + maxChartAreaHeight; - - // Move to next point - left = box.right; - } - } - - helpers.each(leftBoxes.concat(topBoxes), placeBox); - - // Account for chart width and height - left += maxChartAreaWidth; - top += maxChartAreaHeight; - - helpers.each(rightBoxes, placeBox); - helpers.each(bottomBoxes, placeBox); - - // Step 8 - chart.chartArea = { - left: totalLeftBoxesWidth, - top: totalTopBoxesHeight, - right: totalLeftBoxesWidth + maxChartAreaWidth, - bottom: totalTopBoxesHeight + maxChartAreaHeight - }; - - // Step 9 - helpers.each(chartAreaBoxes, function(box) { - box.left = chart.chartArea.left; - box.top = chart.chartArea.top; - box.right = chart.chartArea.right; - box.bottom = chart.chartArea.bottom; - - box.update(maxChartAreaWidth, maxChartAreaHeight); - }); - } - }; -}; diff --git a/src/core/core.layouts.js b/src/core/core.layouts.js new file mode 100644 index 00000000000..b99612bbebd --- /dev/null +++ b/src/core/core.layouts.js @@ -0,0 +1,419 @@ +'use strict'; + +var helpers = require('../helpers/index'); + +function filterByPosition(array, position) { + return helpers.where(array, function(v) { + return v.position === position; + }); +} + +function sortByWeight(array, reverse) { + array.forEach(function(v, i) { + v._tmpIndex_ = i; + return v; + }); + array.sort(function(a, b) { + var v0 = reverse ? b : a; + var v1 = reverse ? a : b; + return v0.weight === v1.weight ? + v0._tmpIndex_ - v1._tmpIndex_ : + v0.weight - v1.weight; + }); + array.forEach(function(v) { + delete v._tmpIndex_; + }); +} + +/** + * @interface ILayoutItem + * @prop {String} position - The position of the item in the chart layout. Possible values are + * 'left', 'top', 'right', 'bottom', and 'chartArea' + * @prop {Number} weight - The weight used to sort the item. Higher weights are further away from the chart area + * @prop {Boolean} fullWidth - if true, and the item is horizontal, then push vertical boxes down + * @prop {Function} isHorizontal - returns true if the layout item is horizontal (ie. top or bottom) + * @prop {Function} update - Takes two parameters: width and height. Returns size of item + * @prop {Function} getPadding - Returns an object with padding on the edges + * @prop {Number} width - Width of item. Must be valid after update() + * @prop {Number} height - Height of item. Must be valid after update() + * @prop {Number} left - Left edge of the item. Set by layout system and cannot be used in update + * @prop {Number} top - Top edge of the item. Set by layout system and cannot be used in update + * @prop {Number} right - Right edge of the item. Set by layout system and cannot be used in update + * @prop {Number} bottom - Bottom edge of the item. Set by layout system and cannot be used in update + */ + +// The layout service is very self explanatory. It's responsible for the layout within a chart. +// Scales, Legends and Plugins all rely on the layout service and can easily register to be placed anywhere they need +// It is this service's responsibility of carrying out that layout. +module.exports = { + defaults: {}, + + /** + * Register a box to a chart. + * A box is simply a reference to an object that requires layout. eg. Scales, Legend, Title. + * @param {Chart} chart - the chart to use + * @param {ILayoutItem} item - the item to add to be layed out + */ + addBox: function(chart, item) { + if (!chart.boxes) { + chart.boxes = []; + } + + // initialize item with default values + item.fullWidth = item.fullWidth || false; + item.position = item.position || 'top'; + item.weight = item.weight || 0; + + chart.boxes.push(item); + }, + + /** + * Remove a layoutItem from a chart + * @param {Chart} chart - the chart to remove the box from + * @param {Object} layoutItem - the item to remove from the layout + */ + removeBox: function(chart, layoutItem) { + var index = chart.boxes ? chart.boxes.indexOf(layoutItem) : -1; + if (index !== -1) { + chart.boxes.splice(index, 1); + } + }, + + /** + * Sets (or updates) options on the given `item`. + * @param {Chart} chart - the chart in which the item lives (or will be added to) + * @param {Object} item - the item to configure with the given options + * @param {Object} options - the new item options. + */ + configure: function(chart, item, options) { + var props = ['fullWidth', 'position', 'weight']; + var ilen = props.length; + var i = 0; + var prop; + + for (; i < ilen; ++i) { + prop = props[i]; + if (options.hasOwnProperty(prop)) { + item[prop] = options[prop]; + } + } + }, + + /** + * Fits boxes of the given chart into the given size by having each box measure itself + * then running a fitting algorithm + * @param {Chart} chart - the chart + * @param {Number} width - the width to fit into + * @param {Number} height - the height to fit into + */ + update: function(chart, width, height) { + if (!chart) { + return; + } + + var layoutOptions = chart.options.layout || {}; + var padding = helpers.options.toPadding(layoutOptions.padding); + var leftPadding = padding.left; + var rightPadding = padding.right; + var topPadding = padding.top; + var bottomPadding = padding.bottom; + + var leftBoxes = filterByPosition(chart.boxes, 'left'); + var rightBoxes = filterByPosition(chart.boxes, 'right'); + var topBoxes = filterByPosition(chart.boxes, 'top'); + var bottomBoxes = filterByPosition(chart.boxes, 'bottom'); + var chartAreaBoxes = filterByPosition(chart.boxes, 'chartArea'); + + // Sort boxes by weight. A higher weight is further away from the chart area + sortByWeight(leftBoxes, true); + sortByWeight(rightBoxes, false); + sortByWeight(topBoxes, true); + sortByWeight(bottomBoxes, false); + + // Essentially we now have any number of boxes on each of the 4 sides. + // Our canvas looks like the following. + // The areas L1 and L2 are the left axes. R1 is the right axis, T1 is the top axis and + // B1 is the bottom axis + // There are also 4 quadrant-like locations (left to right instead of clockwise) reserved for chart overlays + // These locations are single-box locations only, when trying to register a chartArea location that is already taken, + // an error will be thrown. + // + // |----------------------------------------------------| + // | T1 (Full Width) | + // |----------------------------------------------------| + // | | | T2 | | + // | |----|-------------------------------------|----| + // | | | C1 | | C2 | | + // | | |----| |----| | + // | | | | | + // | L1 | L2 | ChartArea (C0) | R1 | + // | | | | | + // | | |----| |----| | + // | | | C3 | | C4 | | + // | |----|-------------------------------------|----| + // | | | B1 | | + // |----------------------------------------------------| + // | B2 (Full Width) | + // |----------------------------------------------------| + // + // What we do to find the best sizing, we do the following + // 1. Determine the minimum size of the chart area. + // 2. Split the remaining width equally between each vertical axis + // 3. Split the remaining height equally between each horizontal axis + // 4. Give each layout the maximum size it can be. The layout will return it's minimum size + // 5. Adjust the sizes of each axis based on it's minimum reported size. + // 6. Refit each axis + // 7. Position each axis in the final location + // 8. Tell the chart the final location of the chart area + // 9. Tell any axes that overlay the chart area the positions of the chart area + + // Step 1 + var chartWidth = width - leftPadding - rightPadding; + var chartHeight = height - topPadding - bottomPadding; + var chartAreaWidth = chartWidth / 2; // min 50% + var chartAreaHeight = chartHeight / 2; // min 50% + + // Step 2 + var verticalBoxWidth = (width - chartAreaWidth) / (leftBoxes.length + rightBoxes.length); + + // Step 3 + var horizontalBoxHeight = (height - chartAreaHeight) / (topBoxes.length + bottomBoxes.length); + + // Step 4 + var maxChartAreaWidth = chartWidth; + var maxChartAreaHeight = chartHeight; + var minBoxSizes = []; + + function getMinimumBoxSize(box) { + var minSize; + var isHorizontal = box.isHorizontal(); + + if (isHorizontal) { + minSize = box.update(box.fullWidth ? chartWidth : maxChartAreaWidth, horizontalBoxHeight); + maxChartAreaHeight -= minSize.height; + } else { + minSize = box.update(verticalBoxWidth, maxChartAreaHeight); + maxChartAreaWidth -= minSize.width; + } + + minBoxSizes.push({ + horizontal: isHorizontal, + minSize: minSize, + box: box, + }); + } + + helpers.each(leftBoxes.concat(rightBoxes, topBoxes, bottomBoxes), getMinimumBoxSize); + + // If a horizontal box has padding, we move the left boxes over to avoid ugly charts (see issue #2478) + var maxHorizontalLeftPadding = 0; + var maxHorizontalRightPadding = 0; + var maxVerticalTopPadding = 0; + var maxVerticalBottomPadding = 0; + + helpers.each(topBoxes.concat(bottomBoxes), function(horizontalBox) { + if (horizontalBox.getPadding) { + var boxPadding = horizontalBox.getPadding(); + maxHorizontalLeftPadding = Math.max(maxHorizontalLeftPadding, boxPadding.left); + maxHorizontalRightPadding = Math.max(maxHorizontalRightPadding, boxPadding.right); + } + }); + + helpers.each(leftBoxes.concat(rightBoxes), function(verticalBox) { + if (verticalBox.getPadding) { + var boxPadding = verticalBox.getPadding(); + maxVerticalTopPadding = Math.max(maxVerticalTopPadding, boxPadding.top); + maxVerticalBottomPadding = Math.max(maxVerticalBottomPadding, boxPadding.bottom); + } + }); + + // At this point, maxChartAreaHeight and maxChartAreaWidth are the size the chart area could + // be if the axes are drawn at their minimum sizes. + // Steps 5 & 6 + var totalLeftBoxesWidth = leftPadding; + var totalRightBoxesWidth = rightPadding; + var totalTopBoxesHeight = topPadding; + var totalBottomBoxesHeight = bottomPadding; + + // Function to fit a box + function fitBox(box) { + var minBoxSize = helpers.findNextWhere(minBoxSizes, function(minBox) { + return minBox.box === box; + }); + + if (minBoxSize) { + if (box.isHorizontal()) { + var scaleMargin = { + left: Math.max(totalLeftBoxesWidth, maxHorizontalLeftPadding), + right: Math.max(totalRightBoxesWidth, maxHorizontalRightPadding), + top: 0, + bottom: 0 + }; + + // Don't use min size here because of label rotation. When the labels are rotated, their rotation highly depends + // on the margin. Sometimes they need to increase in size slightly + box.update(box.fullWidth ? chartWidth : maxChartAreaWidth, chartHeight / 2, scaleMargin); + } else { + box.update(minBoxSize.minSize.width, maxChartAreaHeight); + } + } + } + + // Update, and calculate the left and right margins for the horizontal boxes + helpers.each(leftBoxes.concat(rightBoxes), fitBox); + + helpers.each(leftBoxes, function(box) { + totalLeftBoxesWidth += box.width; + }); + + helpers.each(rightBoxes, function(box) { + totalRightBoxesWidth += box.width; + }); + + // Set the Left and Right margins for the horizontal boxes + helpers.each(topBoxes.concat(bottomBoxes), fitBox); + + // Figure out how much margin is on the top and bottom of the vertical boxes + helpers.each(topBoxes, function(box) { + totalTopBoxesHeight += box.height; + }); + + helpers.each(bottomBoxes, function(box) { + totalBottomBoxesHeight += box.height; + }); + + function finalFitVerticalBox(box) { + var minBoxSize = helpers.findNextWhere(minBoxSizes, function(minSize) { + return minSize.box === box; + }); + + var scaleMargin = { + left: 0, + right: 0, + top: totalTopBoxesHeight, + bottom: totalBottomBoxesHeight + }; + + if (minBoxSize) { + box.update(minBoxSize.minSize.width, maxChartAreaHeight, scaleMargin); + } + } + + // Let the left layout know the final margin + helpers.each(leftBoxes.concat(rightBoxes), finalFitVerticalBox); + + // Recalculate because the size of each layout might have changed slightly due to the margins (label rotation for instance) + totalLeftBoxesWidth = leftPadding; + totalRightBoxesWidth = rightPadding; + totalTopBoxesHeight = topPadding; + totalBottomBoxesHeight = bottomPadding; + + helpers.each(leftBoxes, function(box) { + totalLeftBoxesWidth += box.width; + }); + + helpers.each(rightBoxes, function(box) { + totalRightBoxesWidth += box.width; + }); + + helpers.each(topBoxes, function(box) { + totalTopBoxesHeight += box.height; + }); + helpers.each(bottomBoxes, function(box) { + totalBottomBoxesHeight += box.height; + }); + + // We may be adding some padding to account for rotated x axis labels + var leftPaddingAddition = Math.max(maxHorizontalLeftPadding - totalLeftBoxesWidth, 0); + totalLeftBoxesWidth += leftPaddingAddition; + totalRightBoxesWidth += Math.max(maxHorizontalRightPadding - totalRightBoxesWidth, 0); + + var topPaddingAddition = Math.max(maxVerticalTopPadding - totalTopBoxesHeight, 0); + totalTopBoxesHeight += topPaddingAddition; + totalBottomBoxesHeight += Math.max(maxVerticalBottomPadding - totalBottomBoxesHeight, 0); + + // Figure out if our chart area changed. This would occur if the dataset layout label rotation + // changed due to the application of the margins in step 6. Since we can only get bigger, this is safe to do + // without calling `fit` again + var newMaxChartAreaHeight = height - totalTopBoxesHeight - totalBottomBoxesHeight; + var newMaxChartAreaWidth = width - totalLeftBoxesWidth - totalRightBoxesWidth; + + if (newMaxChartAreaWidth !== maxChartAreaWidth || newMaxChartAreaHeight !== maxChartAreaHeight) { + helpers.each(leftBoxes, function(box) { + box.height = newMaxChartAreaHeight; + }); + + helpers.each(rightBoxes, function(box) { + box.height = newMaxChartAreaHeight; + }); + + helpers.each(topBoxes, function(box) { + if (!box.fullWidth) { + box.width = newMaxChartAreaWidth; + } + }); + + helpers.each(bottomBoxes, function(box) { + if (!box.fullWidth) { + box.width = newMaxChartAreaWidth; + } + }); + + maxChartAreaHeight = newMaxChartAreaHeight; + maxChartAreaWidth = newMaxChartAreaWidth; + } + + // Step 7 - Position the boxes + var left = leftPadding + leftPaddingAddition; + var top = topPadding + topPaddingAddition; + + function placeBox(box) { + if (box.isHorizontal()) { + box.left = box.fullWidth ? leftPadding : totalLeftBoxesWidth; + box.right = box.fullWidth ? width - rightPadding : totalLeftBoxesWidth + maxChartAreaWidth; + box.top = top; + box.bottom = top + box.height; + + // Move to next point + top = box.bottom; + + } else { + + box.left = left; + box.right = left + box.width; + box.top = totalTopBoxesHeight; + box.bottom = totalTopBoxesHeight + maxChartAreaHeight; + + // Move to next point + left = box.right; + } + } + + helpers.each(leftBoxes.concat(topBoxes), placeBox); + + // Account for chart width and height + left += maxChartAreaWidth; + top += maxChartAreaHeight; + + helpers.each(rightBoxes, placeBox); + helpers.each(bottomBoxes, placeBox); + + // Step 8 + chart.chartArea = { + left: totalLeftBoxesWidth, + top: totalTopBoxesHeight, + right: totalLeftBoxesWidth + maxChartAreaWidth, + bottom: totalTopBoxesHeight + maxChartAreaHeight + }; + + // Step 9 + helpers.each(chartAreaBoxes, function(box) { + box.left = chart.chartArea.left; + box.top = chart.chartArea.top; + box.right = chart.chartArea.right; + box.bottom = chart.chartArea.bottom; + + box.update(maxChartAreaWidth, maxChartAreaHeight); + }); + } +}; diff --git a/src/core/core.plugin.js b/src/core/core.plugin.js deleted file mode 100644 index 399075b812c..00000000000 --- a/src/core/core.plugin.js +++ /dev/null @@ -1,374 +0,0 @@ -'use strict'; - -var defaults = require('./core.defaults'); -var Element = require('./core.element'); -var helpers = require('../helpers/index'); - -defaults._set('global', { - plugins: {} -}); - -module.exports = function(Chart) { - - /** - * The plugin service singleton - * @namespace Chart.plugins - * @since 2.1.0 - */ - Chart.plugins = { - /** - * Globally registered plugins. - * @private - */ - _plugins: [], - - /** - * This identifier is used to invalidate the descriptors cache attached to each chart - * when a global plugin is registered or unregistered. In this case, the cache ID is - * incremented and descriptors are regenerated during following API calls. - * @private - */ - _cacheId: 0, - - /** - * Registers the given plugin(s) if not already registered. - * @param {Array|Object} plugins plugin instance(s). - */ - register: function(plugins) { - var p = this._plugins; - ([]).concat(plugins).forEach(function(plugin) { - if (p.indexOf(plugin) === -1) { - p.push(plugin); - } - }); - - this._cacheId++; - }, - - /** - * Unregisters the given plugin(s) only if registered. - * @param {Array|Object} plugins plugin instance(s). - */ - unregister: function(plugins) { - var p = this._plugins; - ([]).concat(plugins).forEach(function(plugin) { - var idx = p.indexOf(plugin); - if (idx !== -1) { - p.splice(idx, 1); - } - }); - - this._cacheId++; - }, - - /** - * Remove all registered plugins. - * @since 2.1.5 - */ - clear: function() { - this._plugins = []; - this._cacheId++; - }, - - /** - * Returns the number of registered plugins? - * @returns {Number} - * @since 2.1.5 - */ - count: function() { - return this._plugins.length; - }, - - /** - * Returns all registered plugin instances. - * @returns {Array} array of plugin objects. - * @since 2.1.5 - */ - getAll: function() { - return this._plugins; - }, - - /** - * Calls enabled plugins for `chart` on the specified hook and with the given args. - * This method immediately returns as soon as a plugin explicitly returns false. The - * returned value can be used, for instance, to interrupt the current action. - * @param {Object} chart - The chart instance for which plugins should be called. - * @param {String} hook - The name of the plugin method to call (e.g. 'beforeUpdate'). - * @param {Array} [args] - Extra arguments to apply to the hook call. - * @returns {Boolean} false if any of the plugins return false, else returns true. - */ - notify: function(chart, hook, args) { - var descriptors = this.descriptors(chart); - var ilen = descriptors.length; - var i, descriptor, plugin, params, method; - - for (i = 0; i < ilen; ++i) { - descriptor = descriptors[i]; - plugin = descriptor.plugin; - method = plugin[hook]; - if (typeof method === 'function') { - params = [chart].concat(args || []); - params.push(descriptor.options); - if (method.apply(plugin, params) === false) { - return false; - } - } - } - - return true; - }, - - /** - * Returns descriptors of enabled plugins for the given chart. - * @returns {Array} [{ plugin, options }] - * @private - */ - descriptors: function(chart) { - var cache = chart._plugins || (chart._plugins = {}); - if (cache.id === this._cacheId) { - return cache.descriptors; - } - - var plugins = []; - var descriptors = []; - var config = (chart && chart.config) || {}; - var options = (config.options && config.options.plugins) || {}; - - this._plugins.concat(config.plugins || []).forEach(function(plugin) { - var idx = plugins.indexOf(plugin); - if (idx !== -1) { - return; - } - - var id = plugin.id; - var opts = options[id]; - if (opts === false) { - return; - } - - if (opts === true) { - opts = helpers.clone(defaults.global.plugins[id]); - } - - plugins.push(plugin); - descriptors.push({ - plugin: plugin, - options: opts || {} - }); - }); - - cache.descriptors = descriptors; - cache.id = this._cacheId; - return descriptors; - } - }; - - /** - * Plugin extension hooks. - * @interface IPlugin - * @since 2.1.0 - */ - /** - * @method IPlugin#beforeInit - * @desc Called before initializing `chart`. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#afterInit - * @desc Called after `chart` has been initialized and before the first update. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#beforeUpdate - * @desc Called before updating `chart`. If any plugin returns `false`, the update - * is cancelled (and thus subsequent render(s)) until another `update` is triggered. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart update. - */ - /** - * @method IPlugin#afterUpdate - * @desc Called after `chart` has been updated and before rendering. Note that this - * hook will not be called if the chart update has been previously cancelled. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#beforeDatasetsUpdate - * @desc Called before updating the `chart` datasets. If any plugin returns `false`, - * the datasets update is cancelled until another `update` is triggered. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - * @returns {Boolean} false to cancel the datasets update. - * @since version 2.1.5 - */ - /** - * @method IPlugin#afterDatasetsUpdate - * @desc Called after the `chart` datasets have been updated. Note that this hook - * will not be called if the datasets update has been previously cancelled. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - * @since version 2.1.5 - */ - /** - * @method IPlugin#beforeDatasetUpdate - * @desc Called before updating the `chart` dataset at the given `args.index`. If any plugin - * returns `false`, the datasets update is cancelled until another `update` is triggered. - * @param {Chart} chart - The chart instance. - * @param {Object} args - The call arguments. - * @param {Number} args.index - The dataset index. - * @param {Object} args.meta - The dataset metadata. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart datasets drawing. - */ - /** - * @method IPlugin#afterDatasetUpdate - * @desc Called after the `chart` datasets at the given `args.index` has been updated. Note - * that this hook will not be called if the datasets update has been previously cancelled. - * @param {Chart} chart - The chart instance. - * @param {Object} args - The call arguments. - * @param {Number} args.index - The dataset index. - * @param {Object} args.meta - The dataset metadata. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#beforeLayout - * @desc Called before laying out `chart`. If any plugin returns `false`, - * the layout update is cancelled until another `update` is triggered. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart layout. - */ - /** - * @method IPlugin#afterLayout - * @desc Called after the `chart` has been layed out. Note that this hook will not - * be called if the layout update has been previously cancelled. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#beforeRender - * @desc Called before rendering `chart`. If any plugin returns `false`, - * the rendering is cancelled until another `render` is triggered. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart rendering. - */ - /** - * @method IPlugin#afterRender - * @desc Called after the `chart` has been fully rendered (and animation completed). Note - * that this hook will not be called if the rendering has been previously cancelled. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#beforeDraw - * @desc Called before drawing `chart` at every animation frame specified by the given - * easing value. If any plugin returns `false`, the frame drawing is cancelled until - * another `render` is triggered. - * @param {Chart.Controller} chart - The chart instance. - * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart drawing. - */ - /** - * @method IPlugin#afterDraw - * @desc Called after the `chart` has been drawn for the specific easing value. Note - * that this hook will not be called if the drawing has been previously cancelled. - * @param {Chart.Controller} chart - The chart instance. - * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#beforeDatasetsDraw - * @desc Called before drawing the `chart` datasets. If any plugin returns `false`, - * the datasets drawing is cancelled until another `render` is triggered. - * @param {Chart.Controller} chart - The chart instance. - * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart datasets drawing. - */ - /** - * @method IPlugin#afterDatasetsDraw - * @desc Called after the `chart` datasets have been drawn. Note that this hook - * will not be called if the datasets drawing has been previously cancelled. - * @param {Chart.Controller} chart - The chart instance. - * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#beforeDatasetDraw - * @desc Called before drawing the `chart` dataset at the given `args.index` (datasets - * are drawn in the reverse order). If any plugin returns `false`, the datasets drawing - * is cancelled until another `render` is triggered. - * @param {Chart} chart - The chart instance. - * @param {Object} args - The call arguments. - * @param {Number} args.index - The dataset index. - * @param {Object} args.meta - The dataset metadata. - * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. - * @returns {Boolean} `false` to cancel the chart datasets drawing. - */ - /** - * @method IPlugin#afterDatasetDraw - * @desc Called after the `chart` datasets at the given `args.index` have been drawn - * (datasets are drawn in the reverse order). Note that this hook will not be called - * if the datasets drawing has been previously cancelled. - * @param {Chart} chart - The chart instance. - * @param {Object} args - The call arguments. - * @param {Number} args.index - The dataset index. - * @param {Object} args.meta - The dataset metadata. - * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#beforeEvent - * @desc Called before processing the specified `event`. If any plugin returns `false`, - * the event will be discarded. - * @param {Chart.Controller} chart - The chart instance. - * @param {IEvent} event - The event object. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#afterEvent - * @desc Called after the `event` has been consumed. Note that this hook - * will not be called if the `event` has been previously discarded. - * @param {Chart.Controller} chart - The chart instance. - * @param {IEvent} event - The event object. - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#resize - * @desc Called after the chart as been resized. - * @param {Chart.Controller} chart - The chart instance. - * @param {Number} size - The new canvas display size (eq. canvas.style width & height). - * @param {Object} options - The plugin options. - */ - /** - * @method IPlugin#destroy - * @desc Called after the chart as been destroyed. - * @param {Chart.Controller} chart - The chart instance. - * @param {Object} options - The plugin options. - */ - - /** - * Provided for backward compatibility, use Chart.plugins instead - * @namespace Chart.pluginService - * @deprecated since version 2.1.5 - * @todo remove at version 3 - * @private - */ - Chart.pluginService = Chart.plugins; - - /** - * Provided for backward compatibility, inheriting from Chart.PlugingBase has no - * effect, instead simply create/register plugins via plain JavaScript objects. - * @interface Chart.PluginBase - * @deprecated since version 2.5.0 - * @todo remove at version 3 - * @private - */ - Chart.PluginBase = Element.extend({}); -}; diff --git a/src/core/core.plugins.js b/src/core/core.plugins.js new file mode 100644 index 00000000000..f2fbcadec31 --- /dev/null +++ b/src/core/core.plugins.js @@ -0,0 +1,382 @@ +'use strict'; + +var defaults = require('./core.defaults'); +var helpers = require('../helpers/index'); + +defaults._set('global', { + plugins: {} +}); + +/** + * The plugin service singleton + * @namespace Chart.plugins + * @since 2.1.0 + */ +module.exports = { + /** + * Globally registered plugins. + * @private + */ + _plugins: [], + + /** + * This identifier is used to invalidate the descriptors cache attached to each chart + * when a global plugin is registered or unregistered. In this case, the cache ID is + * incremented and descriptors are regenerated during following API calls. + * @private + */ + _cacheId: 0, + + /** + * Registers the given plugin(s) if not already registered. + * @param {Array|Object} plugins plugin instance(s). + */ + register: function(plugins) { + var p = this._plugins; + ([]).concat(plugins).forEach(function(plugin) { + if (p.indexOf(plugin) === -1) { + p.push(plugin); + } + }); + + this._cacheId++; + }, + + /** + * Unregisters the given plugin(s) only if registered. + * @param {Array|Object} plugins plugin instance(s). + */ + unregister: function(plugins) { + var p = this._plugins; + ([]).concat(plugins).forEach(function(plugin) { + var idx = p.indexOf(plugin); + if (idx !== -1) { + p.splice(idx, 1); + } + }); + + this._cacheId++; + }, + + /** + * Remove all registered plugins. + * @since 2.1.5 + */ + clear: function() { + this._plugins = []; + this._cacheId++; + }, + + /** + * Returns the number of registered plugins? + * @returns {Number} + * @since 2.1.5 + */ + count: function() { + return this._plugins.length; + }, + + /** + * Returns all registered plugin instances. + * @returns {Array} array of plugin objects. + * @since 2.1.5 + */ + getAll: function() { + return this._plugins; + }, + + /** + * Calls enabled plugins for `chart` on the specified hook and with the given args. + * This method immediately returns as soon as a plugin explicitly returns false. The + * returned value can be used, for instance, to interrupt the current action. + * @param {Object} chart - The chart instance for which plugins should be called. + * @param {String} hook - The name of the plugin method to call (e.g. 'beforeUpdate'). + * @param {Array} [args] - Extra arguments to apply to the hook call. + * @returns {Boolean} false if any of the plugins return false, else returns true. + */ + notify: function(chart, hook, args) { + var descriptors = this.descriptors(chart); + var ilen = descriptors.length; + var i, descriptor, plugin, params, method; + + for (i = 0; i < ilen; ++i) { + descriptor = descriptors[i]; + plugin = descriptor.plugin; + method = plugin[hook]; + if (typeof method === 'function') { + params = [chart].concat(args || []); + params.push(descriptor.options); + if (method.apply(plugin, params) === false) { + return false; + } + } + } + + return true; + }, + + /** + * Returns descriptors of enabled plugins for the given chart. + * @returns {Array} [{ plugin, options }] + * @private + */ + descriptors: function(chart) { + var cache = chart.$plugins || (chart.$plugins = {}); + if (cache.id === this._cacheId) { + return cache.descriptors; + } + + var plugins = []; + var descriptors = []; + var config = (chart && chart.config) || {}; + var options = (config.options && config.options.plugins) || {}; + + this._plugins.concat(config.plugins || []).forEach(function(plugin) { + var idx = plugins.indexOf(plugin); + if (idx !== -1) { + return; + } + + var id = plugin.id; + var opts = options[id]; + if (opts === false) { + return; + } + + if (opts === true) { + opts = helpers.clone(defaults.global.plugins[id]); + } + + plugins.push(plugin); + descriptors.push({ + plugin: plugin, + options: opts || {} + }); + }); + + cache.descriptors = descriptors; + cache.id = this._cacheId; + return descriptors; + }, + + /** + * Invalidates cache for the given chart: descriptors hold a reference on plugin option, + * but in some cases, this reference can be changed by the user when updating options. + * https://github.com/chartjs/Chart.js/issues/5111#issuecomment-355934167 + * @private + */ + _invalidate: function(chart) { + delete chart.$plugins; + } +}; + +/** + * Plugin extension hooks. + * @interface IPlugin + * @since 2.1.0 + */ +/** + * @method IPlugin#beforeInit + * @desc Called before initializing `chart`. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#afterInit + * @desc Called after `chart` has been initialized and before the first update. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#beforeUpdate + * @desc Called before updating `chart`. If any plugin returns `false`, the update + * is cancelled (and thus subsequent render(s)) until another `update` is triggered. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + * @returns {Boolean} `false` to cancel the chart update. + */ +/** + * @method IPlugin#afterUpdate + * @desc Called after `chart` has been updated and before rendering. Note that this + * hook will not be called if the chart update has been previously cancelled. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#beforeDatasetsUpdate + * @desc Called before updating the `chart` datasets. If any plugin returns `false`, + * the datasets update is cancelled until another `update` is triggered. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + * @returns {Boolean} false to cancel the datasets update. + * @since version 2.1.5 +*/ +/** + * @method IPlugin#afterDatasetsUpdate + * @desc Called after the `chart` datasets have been updated. Note that this hook + * will not be called if the datasets update has been previously cancelled. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + * @since version 2.1.5 + */ +/** + * @method IPlugin#beforeDatasetUpdate + * @desc Called before updating the `chart` dataset at the given `args.index`. If any plugin + * returns `false`, the datasets update is cancelled until another `update` is triggered. + * @param {Chart} chart - The chart instance. + * @param {Object} args - The call arguments. + * @param {Number} args.index - The dataset index. + * @param {Object} args.meta - The dataset metadata. + * @param {Object} options - The plugin options. + * @returns {Boolean} `false` to cancel the chart datasets drawing. + */ +/** + * @method IPlugin#afterDatasetUpdate + * @desc Called after the `chart` datasets at the given `args.index` has been updated. Note + * that this hook will not be called if the datasets update has been previously cancelled. + * @param {Chart} chart - The chart instance. + * @param {Object} args - The call arguments. + * @param {Number} args.index - The dataset index. + * @param {Object} args.meta - The dataset metadata. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#beforeLayout + * @desc Called before laying out `chart`. If any plugin returns `false`, + * the layout update is cancelled until another `update` is triggered. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + * @returns {Boolean} `false` to cancel the chart layout. + */ +/** + * @method IPlugin#afterLayout + * @desc Called after the `chart` has been layed out. Note that this hook will not + * be called if the layout update has been previously cancelled. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#beforeRender + * @desc Called before rendering `chart`. If any plugin returns `false`, + * the rendering is cancelled until another `render` is triggered. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + * @returns {Boolean} `false` to cancel the chart rendering. + */ +/** + * @method IPlugin#afterRender + * @desc Called after the `chart` has been fully rendered (and animation completed). Note + * that this hook will not be called if the rendering has been previously cancelled. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#beforeDraw + * @desc Called before drawing `chart` at every animation frame specified by the given + * easing value. If any plugin returns `false`, the frame drawing is cancelled until + * another `render` is triggered. + * @param {Chart.Controller} chart - The chart instance. + * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. + * @param {Object} options - The plugin options. + * @returns {Boolean} `false` to cancel the chart drawing. + */ +/** + * @method IPlugin#afterDraw + * @desc Called after the `chart` has been drawn for the specific easing value. Note + * that this hook will not be called if the drawing has been previously cancelled. + * @param {Chart.Controller} chart - The chart instance. + * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#beforeDatasetsDraw + * @desc Called before drawing the `chart` datasets. If any plugin returns `false`, + * the datasets drawing is cancelled until another `render` is triggered. + * @param {Chart.Controller} chart - The chart instance. + * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. + * @param {Object} options - The plugin options. + * @returns {Boolean} `false` to cancel the chart datasets drawing. + */ +/** + * @method IPlugin#afterDatasetsDraw + * @desc Called after the `chart` datasets have been drawn. Note that this hook + * will not be called if the datasets drawing has been previously cancelled. + * @param {Chart.Controller} chart - The chart instance. + * @param {Number} easingValue - The current animation value, between 0.0 and 1.0. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#beforeDatasetDraw + * @desc Called before drawing the `chart` dataset at the given `args.index` (datasets + * are drawn in the reverse order). If any plugin returns `false`, the datasets drawing + * is cancelled until another `render` is triggered. + * @param {Chart} chart - The chart instance. + * @param {Object} args - The call arguments. + * @param {Number} args.index - The dataset index. + * @param {Object} args.meta - The dataset metadata. + * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. + * @param {Object} options - The plugin options. + * @returns {Boolean} `false` to cancel the chart datasets drawing. + */ +/** + * @method IPlugin#afterDatasetDraw + * @desc Called after the `chart` datasets at the given `args.index` have been drawn + * (datasets are drawn in the reverse order). Note that this hook will not be called + * if the datasets drawing has been previously cancelled. + * @param {Chart} chart - The chart instance. + * @param {Object} args - The call arguments. + * @param {Number} args.index - The dataset index. + * @param {Object} args.meta - The dataset metadata. + * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#beforeTooltipDraw + * @desc Called before drawing the `tooltip`. If any plugin returns `false`, + * the tooltip drawing is cancelled until another `render` is triggered. + * @param {Chart} chart - The chart instance. + * @param {Object} args - The call arguments. + * @param {Object} args.tooltip - The tooltip. + * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. + * @param {Object} options - The plugin options. + * @returns {Boolean} `false` to cancel the chart tooltip drawing. + */ +/** + * @method IPlugin#afterTooltipDraw + * @desc Called after drawing the `tooltip`. Note that this hook will not + * be called if the tooltip drawing has been previously cancelled. + * @param {Chart} chart - The chart instance. + * @param {Object} args - The call arguments. + * @param {Object} args.tooltip - The tooltip. + * @param {Number} args.easingValue - The current animation value, between 0.0 and 1.0. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#beforeEvent + * @desc Called before processing the specified `event`. If any plugin returns `false`, + * the event will be discarded. + * @param {Chart.Controller} chart - The chart instance. + * @param {IEvent} event - The event object. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#afterEvent + * @desc Called after the `event` has been consumed. Note that this hook + * will not be called if the `event` has been previously discarded. + * @param {Chart.Controller} chart - The chart instance. + * @param {IEvent} event - The event object. + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#resize + * @desc Called after the chart as been resized. + * @param {Chart.Controller} chart - The chart instance. + * @param {Number} size - The new canvas display size (eq. canvas.style width & height). + * @param {Object} options - The plugin options. + */ +/** + * @method IPlugin#destroy + * @desc Called after the chart as been destroyed. + * @param {Chart.Controller} chart - The chart instance. + * @param {Object} options - The plugin options. + */ diff --git a/src/core/core.scaleService.js b/src/core/core.scaleService.js index 23cabe610d2..f2ea01d329a 100644 --- a/src/core/core.scaleService.js +++ b/src/core/core.scaleService.js @@ -2,6 +2,7 @@ var defaults = require('./core.defaults'); var helpers = require('../helpers/index'); +var layouts = require('./core.layouts'); module.exports = function(Chart) { @@ -38,7 +39,7 @@ module.exports = function(Chart) { scale.fullWidth = scale.options.fullWidth; scale.position = scale.options.position; scale.weight = scale.options.weight; - Chart.layoutService.addBox(chart, scale); + layouts.addBox(chart, scale); }); } }; diff --git a/src/core/core.ticks.js b/src/core/core.ticks.js index 2dbc27474d4..3898842a49a 100644 --- a/src/core/core.ticks.js +++ b/src/core/core.ticks.js @@ -7,140 +7,6 @@ var helpers = require('../helpers/index'); * @namespace Chart.Ticks */ module.exports = { - /** - * Namespace to hold generators for different types of ticks - * @namespace Chart.Ticks.generators - */ - generators: { - /** - * Interface for the options provided to the numeric tick generator - * @interface INumericTickGenerationOptions - */ - /** - * The maximum number of ticks to display - * @name INumericTickGenerationOptions#maxTicks - * @type Number - */ - /** - * The distance between each tick. - * @name INumericTickGenerationOptions#stepSize - * @type Number - * @optional - */ - /** - * Forced minimum for the ticks. If not specified, the minimum of the data range is used to calculate the tick minimum - * @name INumericTickGenerationOptions#min - * @type Number - * @optional - */ - /** - * The maximum value of the ticks. If not specified, the maximum of the data range is used to calculate the tick maximum - * @name INumericTickGenerationOptions#max - * @type Number - * @optional - */ - - /** - * Generate a set of linear ticks - * @method Chart.Ticks.generators.linear - * @param generationOptions {INumericTickGenerationOptions} the options used to generate the ticks - * @param dataRange {IRange} the range of the data - * @returns {Array} array of tick values - */ - linear: function(generationOptions, dataRange) { - var ticks = []; - // To get a "nice" value for the tick spacing, we will use the appropriately named - // "nice number" algorithm. See http://stackoverflow.com/questions/8506881/nice-label-algorithm-for-charts-with-minimum-ticks - // for details. - - var spacing; - if (generationOptions.stepSize && generationOptions.stepSize > 0) { - spacing = generationOptions.stepSize; - } else { - var niceRange = helpers.niceNum(dataRange.max - dataRange.min, false); - spacing = helpers.niceNum(niceRange / (generationOptions.maxTicks - 1), true); - } - var niceMin = Math.floor(dataRange.min / spacing) * spacing; - var niceMax = Math.ceil(dataRange.max / spacing) * spacing; - - // If min, max and stepSize is set and they make an evenly spaced scale use it. - if (generationOptions.min && generationOptions.max && generationOptions.stepSize) { - // If very close to our whole number, use it. - if (helpers.almostWhole((generationOptions.max - generationOptions.min) / generationOptions.stepSize, spacing / 1000)) { - niceMin = generationOptions.min; - niceMax = generationOptions.max; - } - } - - var numSpaces = (niceMax - niceMin) / spacing; - // If very close to our rounded value, use it. - if (helpers.almostEquals(numSpaces, Math.round(numSpaces), spacing / 1000)) { - numSpaces = Math.round(numSpaces); - } else { - numSpaces = Math.ceil(numSpaces); - } - - // Put the values into the ticks array - ticks.push(generationOptions.min !== undefined ? generationOptions.min : niceMin); - for (var j = 1; j < numSpaces; ++j) { - ticks.push(niceMin + (j * spacing)); - } - ticks.push(generationOptions.max !== undefined ? generationOptions.max : niceMax); - - return ticks; - }, - - /** - * Generate a set of logarithmic ticks - * @method Chart.Ticks.generators.logarithmic - * @param generationOptions {INumericTickGenerationOptions} the options used to generate the ticks - * @param dataRange {IRange} the range of the data - * @returns {Array} array of tick values - */ - logarithmic: function(generationOptions, dataRange) { - var ticks = []; - var valueOrDefault = helpers.valueOrDefault; - - // Figure out what the max number of ticks we can support it is based on the size of - // the axis area. For now, we say that the minimum tick spacing in pixels must be 50 - // We also limit the maximum number of ticks to 11 which gives a nice 10 squares on - // the graph - var tickVal = valueOrDefault(generationOptions.min, Math.pow(10, Math.floor(helpers.log10(dataRange.min)))); - - var endExp = Math.floor(helpers.log10(dataRange.max)); - var endSignificand = Math.ceil(dataRange.max / Math.pow(10, endExp)); - var exp, significand; - - if (tickVal === 0) { - exp = Math.floor(helpers.log10(dataRange.minNotZero)); - significand = Math.floor(dataRange.minNotZero / Math.pow(10, exp)); - - ticks.push(tickVal); - tickVal = significand * Math.pow(10, exp); - } else { - exp = Math.floor(helpers.log10(tickVal)); - significand = Math.floor(tickVal / Math.pow(10, exp)); - } - - do { - ticks.push(tickVal); - - ++significand; - if (significand === 10) { - significand = 1; - ++exp; - } - - tickVal = significand * Math.pow(10, exp); - } while (exp < endExp || (exp === endExp && significand < endSignificand)); - - var lastTick = valueOrDefault(generationOptions.max, tickVal); - ticks.push(lastTick); - - return ticks; - } - }, - /** * Namespace to hold formatters for different types of ticks * @namespace Chart.Ticks.formatters diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index 2acd31e051a..9b09d760443 100644 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -299,10 +299,10 @@ module.exports = function(Chart) { } olf = function(x) { - return x + size.width > chart.width; + return x + size.width + model.caretSize + model.caretPadding > chart.width; }; orf = function(x) { - return x - size.width < 0; + return x - size.width - model.caretSize - model.caretPadding < 0; }; yf = function(y) { return y <= midY ? 'top' : 'bottom'; @@ -336,7 +336,7 @@ module.exports = function(Chart) { /** * @Helper to get the location a tooltip needs to be placed at given the initial position (via the vm) and the size and alignment */ - function getBackgroundPoint(vm, size, alignment) { + function getBackgroundPoint(vm, size, alignment, chart) { // Background Position var x = vm.x; var y = vm.y; @@ -353,6 +353,12 @@ module.exports = function(Chart) { x -= size.width; } else if (xAlign === 'center') { x -= (size.width / 2); + if (x + size.width > chart.width) { + x = chart.width - size.width; + } + if (x < 0) { + x = 0; + } } if (yAlign === 'top') { @@ -384,6 +390,7 @@ module.exports = function(Chart) { Chart.Tooltip = Element.extend({ initialize: function() { this._model = getBaseModel(this._options); + this._lastActive = []; }, // Get the title @@ -495,7 +502,7 @@ module.exports = function(Chart) { var labelColors = []; var labelTextColors = []; - tooltipPosition = Chart.Tooltip.positioners[opts.position](active, me._eventPosition); + tooltipPosition = Chart.Tooltip.positioners[opts.position].call(me, active, me._eventPosition); var tooltipItems = []; for (i = 0, len = active.length; i < len; ++i) { @@ -544,7 +551,7 @@ module.exports = function(Chart) { tooltipSize = getTooltipSize(this, model); alignment = determineAlignment(this, tooltipSize); // Final Size and Position - backgroundPoint = getBackgroundPoint(model, tooltipSize, alignment); + backgroundPoint = getBackgroundPoint(model, tooltipSize, alignment, me._chart); } else { model.opacity = 0; } @@ -616,7 +623,7 @@ module.exports = function(Chart) { x1 = x2 - caretSize; x3 = x2 + caretSize; } else { - x2 = ptX + (width / 2); + x2 = vm.caretX; x1 = x2 - caretSize; x3 = x2 + caretSize; } @@ -845,25 +852,19 @@ module.exports = function(Chart) { // Remember Last Actives changed = !helpers.arrayEquals(me._active, me._lastActive); - // If tooltip didn't change, do not handle the target event - if (!changed) { - return false; - } - - me._lastActive = me._active; - - if (options.enabled || options.custom) { - me._eventPosition = { - x: e.x, - y: e.y - }; + // Only handle target event on tooltip change + if (changed) { + me._lastActive = me._active; - var model = me._model; - me.update(true); - me.pivot(); + if (options.enabled || options.custom) { + me._eventPosition = { + x: e.x, + y: e.y + }; - // See if our tooltip position changed - changed |= (model.x !== me._model.x) || (model.y !== me._model.y); + me.update(true); + me.pivot(); + } } return changed; diff --git a/src/elements/element.point.js b/src/elements/element.point.js index d5116a259c5..eab5b31d453 100644 --- a/src/elements/element.point.js +++ b/src/elements/element.point.js @@ -24,12 +24,12 @@ defaults._set('global', { function xRange(mouseX) { var vm = this._view; - return vm ? (Math.pow(mouseX - vm.x, 2) < Math.pow(vm.radius + vm.hitRadius, 2)) : false; + return vm ? (Math.abs(mouseX - vm.x) < vm.radius + vm.hitRadius) : false; } function yRange(mouseY) { var vm = this._view; - return vm ? (Math.pow(mouseY - vm.y, 2) < Math.pow(vm.radius + vm.hitRadius, 2)) : false; + return vm ? (Math.abs(mouseY - vm.y) < vm.radius + vm.hitRadius) : false; } module.exports = Element.extend({ diff --git a/src/helpers/helpers.core.js b/src/helpers/helpers.core.js index 1ada7a754b1..2a4b0098ccf 100644 --- a/src/helpers/helpers.core.js +++ b/src/helpers/helpers.core.js @@ -250,6 +250,48 @@ var helpers = { */ mergeIf: function(target, source) { return helpers.merge(target, source, {merger: helpers._mergerIf}); + }, + + /** + * Applies the contents of two or more objects together into the first object. + * @param {Object} target - The target object in which all objects are merged into. + * @param {Object} arg1 - Object containing additional properties to merge in target. + * @param {Object} argN - Additional objects containing properties to merge in target. + * @returns {Object} The `target` object. + */ + extend: function(target) { + var setFn = function(value, key) { + target[key] = value; + }; + for (var i = 1, ilen = arguments.length; i < ilen; ++i) { + helpers.each(arguments[i], setFn); + } + return target; + }, + + /** + * Basic javascript inheritance based on the model created in Backbone.js + */ + inherits: function(extensions) { + var me = this; + var ChartElement = (extensions && extensions.hasOwnProperty('constructor')) ? extensions.constructor : function() { + return me.apply(this, arguments); + }; + + var Surrogate = function() { + this.constructor = ChartElement; + }; + + Surrogate.prototype = me.prototype; + ChartElement.prototype = new Surrogate(); + ChartElement.extend = helpers.inherits; + + if (extensions) { + helpers.extend(ChartElement.prototype, extensions); + } + + ChartElement.__super__ = me.prototype; + return ChartElement; } }; diff --git a/src/platforms/platform.dom.js b/src/platforms/platform.dom.js index a14119ad425..a882d22a537 100644 --- a/src/platforms/platform.dom.js +++ b/src/platforms/platform.dom.js @@ -236,6 +236,13 @@ function watchForRender(node, handler) { addEventListener(node, type, proxy); }); + // #4737: Chrome might skip the CSS animation when the CSS_RENDER_MONITOR class + // is removed then added back immediately (same animation frame?). Accessing the + // `offsetParent` property will force a reflow and re-evaluate the CSS animation. + // https://gist.github.com/paulirish/5d52fb081b3570c81e3a#box-metrics + // https://github.com/chartjs/Chart.js/issues/4737 + expando.reflow = !!node.offsetParent; + node.classList.add(CSS_RENDER_MONITOR); } diff --git a/src/plugins/index.js b/src/plugins/index.js new file mode 100644 index 00000000000..1cd98151629 --- /dev/null +++ b/src/plugins/index.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = {}; +module.exports.filler = require('./plugin.filler'); +module.exports.legend = require('./plugin.legend'); +module.exports.title = require('./plugin.title'); diff --git a/src/plugins/plugin.filler.js b/src/plugins/plugin.filler.js index cf022654284..eb8dad4c3b0 100644 --- a/src/plugins/plugin.filler.js +++ b/src/plugins/plugin.filler.js @@ -18,304 +18,301 @@ defaults._set('global', { } }); -module.exports = function() { - - var mappers = { - dataset: function(source) { - var index = source.fill; - var chart = source.chart; - var meta = chart.getDatasetMeta(index); - var visible = meta && chart.isDatasetVisible(index); - var points = (visible && meta.dataset._children) || []; - var length = points.length || 0; - - return !length ? null : function(point, i) { - return (i < length && points[i]._view) || null; +var mappers = { + dataset: function(source) { + var index = source.fill; + var chart = source.chart; + var meta = chart.getDatasetMeta(index); + var visible = meta && chart.isDatasetVisible(index); + var points = (visible && meta.dataset._children) || []; + var length = points.length || 0; + + return !length ? null : function(point, i) { + return (i < length && points[i]._view) || null; + }; + }, + + boundary: function(source) { + var boundary = source.boundary; + var x = boundary ? boundary.x : null; + var y = boundary ? boundary.y : null; + + return function(point) { + return { + x: x === null ? point.x : x, + y: y === null ? point.y : y, }; - }, + }; + } +}; - boundary: function(source) { - var boundary = source.boundary; - var x = boundary ? boundary.x : null; - var y = boundary ? boundary.y : null; +// @todo if (fill[0] === '#') +function decodeFill(el, index, count) { + var model = el._model || {}; + var fill = model.fill; + var target; - return function(point) { - return { - x: x === null ? point.x : x, - y: y === null ? point.y : y, - }; - }; - } - }; + if (fill === undefined) { + fill = !!model.backgroundColor; + } + + if (fill === false || fill === null) { + return false; + } - // @todo if (fill[0] === '#') - function decodeFill(el, index, count) { - var model = el._model || {}; - var fill = model.fill; - var target; + if (fill === true) { + return 'origin'; + } - if (fill === undefined) { - fill = !!model.backgroundColor; + target = parseFloat(fill, 10); + if (isFinite(target) && Math.floor(target) === target) { + if (fill[0] === '-' || fill[0] === '+') { + target = index + target; } - if (fill === false || fill === null) { + if (target === index || target < 0 || target >= count) { return false; } - if (fill === true) { - return 'origin'; - } + return target; + } - target = parseFloat(fill, 10); - if (isFinite(target) && Math.floor(target) === target) { - if (fill[0] === '-' || fill[0] === '+') { - target = index + target; - } + switch (fill) { + // compatibility + case 'bottom': + return 'start'; + case 'top': + return 'end'; + case 'zero': + return 'origin'; + // supported boundaries + case 'origin': + case 'start': + case 'end': + return fill; + // invalid fill values + default: + return false; + } +} - if (target === index || target < 0 || target >= count) { - return false; - } +function computeBoundary(source) { + var model = source.el._model || {}; + var scale = source.el._scale || {}; + var fill = source.fill; + var target = null; + var horizontal; - return target; - } - - switch (fill) { - // compatibility - case 'bottom': - return 'start'; - case 'top': - return 'end'; - case 'zero': - return 'origin'; - // supported boundaries - case 'origin': - case 'start': - case 'end': - return fill; - // invalid fill values - default: - return false; - } + if (isFinite(fill)) { + return null; } - function computeBoundary(source) { - var model = source.el._model || {}; - var scale = source.el._scale || {}; - var fill = source.fill; - var target = null; - var horizontal; + // Backward compatibility: until v3, we still need to support boundary values set on + // the model (scaleTop, scaleBottom and scaleZero) because some external plugins and + // controllers might still use it (e.g. the Smith chart). + + if (fill === 'start') { + target = model.scaleBottom === undefined ? scale.bottom : model.scaleBottom; + } else if (fill === 'end') { + target = model.scaleTop === undefined ? scale.top : model.scaleTop; + } else if (model.scaleZero !== undefined) { + target = model.scaleZero; + } else if (scale.getBasePosition) { + target = scale.getBasePosition(); + } else if (scale.getBasePixel) { + target = scale.getBasePixel(); + } - if (isFinite(fill)) { - return null; + if (target !== undefined && target !== null) { + if (target.x !== undefined && target.y !== undefined) { + return target; } - // Backward compatibility: until v3, we still need to support boundary values set on - // the model (scaleTop, scaleBottom and scaleZero) because some external plugins and - // controllers might still use it (e.g. the Smith chart). - - if (fill === 'start') { - target = model.scaleBottom === undefined ? scale.bottom : model.scaleBottom; - } else if (fill === 'end') { - target = model.scaleTop === undefined ? scale.top : model.scaleTop; - } else if (model.scaleZero !== undefined) { - target = model.scaleZero; - } else if (scale.getBasePosition) { - target = scale.getBasePosition(); - } else if (scale.getBasePixel) { - target = scale.getBasePixel(); + if (typeof target === 'number' && isFinite(target)) { + horizontal = scale.isHorizontal(); + return { + x: horizontal ? target : null, + y: horizontal ? null : target + }; } + } - if (target !== undefined && target !== null) { - if (target.x !== undefined && target.y !== undefined) { - return target; - } + return null; +} - if (typeof target === 'number' && isFinite(target)) { - horizontal = scale.isHorizontal(); - return { - x: horizontal ? target : null, - y: horizontal ? null : target - }; - } - } +function resolveTarget(sources, index, propagate) { + var source = sources[index]; + var fill = source.fill; + var visited = [index]; + var target; - return null; + if (!propagate) { + return fill; } - function resolveTarget(sources, index, propagate) { - var source = sources[index]; - var fill = source.fill; - var visited = [index]; - var target; - - if (!propagate) { + while (fill !== false && visited.indexOf(fill) === -1) { + if (!isFinite(fill)) { return fill; } - while (fill !== false && visited.indexOf(fill) === -1) { - if (!isFinite(fill)) { - return fill; - } - - target = sources[fill]; - if (!target) { - return false; - } - - if (target.visible) { - return fill; - } + target = sources[fill]; + if (!target) { + return false; + } - visited.push(fill); - fill = target.fill; + if (target.visible) { + return fill; } - return false; + visited.push(fill); + fill = target.fill; } - function createMapper(source) { - var fill = source.fill; - var type = 'dataset'; - - if (fill === false) { - return null; - } + return false; +} - if (!isFinite(fill)) { - type = 'boundary'; - } +function createMapper(source) { + var fill = source.fill; + var type = 'dataset'; - return mappers[type](source); + if (fill === false) { + return null; } - function isDrawable(point) { - return point && !point.skip; + if (!isFinite(fill)) { + type = 'boundary'; } - function drawArea(ctx, curve0, curve1, len0, len1) { - var i; + return mappers[type](source); +} - if (!len0 || !len1) { - return; - } +function isDrawable(point) { + return point && !point.skip; +} - // building first area curve (normal) - ctx.moveTo(curve0[0].x, curve0[0].y); - for (i = 1; i < len0; ++i) { - helpers.canvas.lineTo(ctx, curve0[i - 1], curve0[i]); - } +function drawArea(ctx, curve0, curve1, len0, len1) { + var i; - // joining the two area curves - ctx.lineTo(curve1[len1 - 1].x, curve1[len1 - 1].y); + if (!len0 || !len1) { + return; + } - // building opposite area curve (reverse) - for (i = len1 - 1; i > 0; --i) { - helpers.canvas.lineTo(ctx, curve1[i], curve1[i - 1], true); - } + // building first area curve (normal) + ctx.moveTo(curve0[0].x, curve0[0].y); + for (i = 1; i < len0; ++i) { + helpers.canvas.lineTo(ctx, curve0[i - 1], curve0[i]); } - function doFill(ctx, points, mapper, view, color, loop) { - var count = points.length; - var span = view.spanGaps; - var curve0 = []; - var curve1 = []; - var len0 = 0; - var len1 = 0; - var i, ilen, index, p0, p1, d0, d1; - - ctx.beginPath(); - - for (i = 0, ilen = (count + !!loop); i < ilen; ++i) { - index = i % count; - p0 = points[index]._view; - p1 = mapper(p0, index, view); - d0 = isDrawable(p0); - d1 = isDrawable(p1); - - if (d0 && d1) { - len0 = curve0.push(p0); - len1 = curve1.push(p1); - } else if (len0 && len1) { - if (!span) { - drawArea(ctx, curve0, curve1, len0, len1); - len0 = len1 = 0; - curve0 = []; - curve1 = []; - } else { - if (d0) { - curve0.push(p0); - } - if (d1) { - curve1.push(p1); - } + // joining the two area curves + ctx.lineTo(curve1[len1 - 1].x, curve1[len1 - 1].y); + + // building opposite area curve (reverse) + for (i = len1 - 1; i > 0; --i) { + helpers.canvas.lineTo(ctx, curve1[i], curve1[i - 1], true); + } +} + +function doFill(ctx, points, mapper, view, color, loop) { + var count = points.length; + var span = view.spanGaps; + var curve0 = []; + var curve1 = []; + var len0 = 0; + var len1 = 0; + var i, ilen, index, p0, p1, d0, d1; + + ctx.beginPath(); + + for (i = 0, ilen = (count + !!loop); i < ilen; ++i) { + index = i % count; + p0 = points[index]._view; + p1 = mapper(p0, index, view); + d0 = isDrawable(p0); + d1 = isDrawable(p1); + + if (d0 && d1) { + len0 = curve0.push(p0); + len1 = curve1.push(p1); + } else if (len0 && len1) { + if (!span) { + drawArea(ctx, curve0, curve1, len0, len1); + len0 = len1 = 0; + curve0 = []; + curve1 = []; + } else { + if (d0) { + curve0.push(p0); + } + if (d1) { + curve1.push(p1); } } } - - drawArea(ctx, curve0, curve1, len0, len1); - - ctx.closePath(); - ctx.fillStyle = color; - ctx.fill(); } - return { - id: 'filler', - - afterDatasetsUpdate: function(chart, options) { - var count = (chart.data.datasets || []).length; - var propagate = options.propagate; - var sources = []; - var meta, i, el, source; - - for (i = 0; i < count; ++i) { - meta = chart.getDatasetMeta(i); - el = meta.dataset; - source = null; - - if (el && el._model && el instanceof elements.Line) { - source = { - visible: chart.isDatasetVisible(i), - fill: decodeFill(el, i, count), - chart: chart, - el: el - }; - } - - meta.$filler = source; - sources.push(source); + drawArea(ctx, curve0, curve1, len0, len1); + + ctx.closePath(); + ctx.fillStyle = color; + ctx.fill(); +} + +module.exports = { + id: 'filler', + + afterDatasetsUpdate: function(chart, options) { + var count = (chart.data.datasets || []).length; + var propagate = options.propagate; + var sources = []; + var meta, i, el, source; + + for (i = 0; i < count; ++i) { + meta = chart.getDatasetMeta(i); + el = meta.dataset; + source = null; + + if (el && el._model && el instanceof elements.Line) { + source = { + visible: chart.isDatasetVisible(i), + fill: decodeFill(el, i, count), + chart: chart, + el: el + }; } - for (i = 0; i < count; ++i) { - source = sources[i]; - if (!source) { - continue; - } + meta.$filler = source; + sources.push(source); + } - source.fill = resolveTarget(sources, i, propagate); - source.boundary = computeBoundary(source); - source.mapper = createMapper(source); + for (i = 0; i < count; ++i) { + source = sources[i]; + if (!source) { + continue; } - }, - beforeDatasetDraw: function(chart, args) { - var meta = args.meta.$filler; - if (!meta) { - return; - } + source.fill = resolveTarget(sources, i, propagate); + source.boundary = computeBoundary(source); + source.mapper = createMapper(source); + } + }, - var ctx = chart.ctx; - var el = meta.el; - var view = el._view; - var points = el._children || []; - var mapper = meta.mapper; - var color = view.backgroundColor || defaults.global.defaultColor; - - if (mapper && color && points.length) { - helpers.canvas.clipArea(ctx, chart.chartArea); - doFill(ctx, points, mapper, view, color, el._loop); - helpers.canvas.unclipArea(ctx); - } + beforeDatasetDraw: function(chart, args) { + var meta = args.meta.$filler; + if (!meta) { + return; + } + + var ctx = chart.ctx; + var el = meta.el; + var view = el._view; + var points = el._children || []; + var mapper = meta.mapper; + var color = view.backgroundColor || defaults.global.defaultColor; + + if (mapper && color && points.length) { + helpers.canvas.clipArea(ctx, chart.chartArea); + doFill(ctx, points, mapper, view, color, el._loop); + helpers.canvas.unclipArea(ctx); } - }; + } }; diff --git a/src/plugins/plugin.legend.js b/src/plugins/plugin.legend.js index 275e3d4f228..1096e722710 100644 --- a/src/plugins/plugin.legend.js +++ b/src/plugins/plugin.legend.js @@ -3,6 +3,9 @@ var defaults = require('../core/core.defaults'); var Element = require('../core/core.element'); var helpers = require('../helpers/index'); +var layouts = require('../core/core.layouts'); + +var noop = helpers.noop; defaults._set('global', { legend: { @@ -11,6 +14,7 @@ defaults._set('global', { fullWidth: true, reverse: false, weight: 1000, + // a callback that will handle onClick: function(e, legendItem) { var index = legendItem.datasetIndex; @@ -85,459 +89,467 @@ defaults._set('global', { } }); -module.exports = function(Chart) { +/** + * IMPORTANT: this class is exposed publicly as Chart.Legend, backward compatibility required! + */ +var Legend = Element.extend({ - var layout = Chart.layoutService; - var noop = helpers.noop; + initialize: function(config) { + helpers.extend(this, config); - Chart.Legend = Element.extend({ + // Contains hit boxes for each dataset (in dataset order) + this.legendHitBoxes = []; - initialize: function(config) { - helpers.extend(this, config); + // Are we in doughnut mode which has a different data type + this.doughnutMode = false; + }, - // Contains hit boxes for each dataset (in dataset order) - this.legendHitBoxes = []; + // These methods are ordered by lifecycle. Utilities then follow. + // Any function defined here is inherited by all legend types. + // Any function can be extended by the legend type + + beforeUpdate: noop, + update: function(maxWidth, maxHeight, margins) { + var me = this; + + // Update Lifecycle - Probably don't want to ever extend or overwrite this function ;) + me.beforeUpdate(); + + // Absorb the master measurements + me.maxWidth = maxWidth; + me.maxHeight = maxHeight; + me.margins = margins; + + // Dimensions + me.beforeSetDimensions(); + me.setDimensions(); + me.afterSetDimensions(); + // Labels + me.beforeBuildLabels(); + me.buildLabels(); + me.afterBuildLabels(); + + // Fit + me.beforeFit(); + me.fit(); + me.afterFit(); + // + me.afterUpdate(); - // Are we in doughnut mode which has a different data type - this.doughnutMode = false; - }, + return me.minSize; + }, + afterUpdate: noop, + + // + + beforeSetDimensions: noop, + setDimensions: function() { + var me = this; + // Set the unconstrained dimension before label rotation + if (me.isHorizontal()) { + // Reset position before calculating rotation + me.width = me.maxWidth; + me.left = 0; + me.right = me.width; + } else { + me.height = me.maxHeight; + + // Reset position before calculating rotation + me.top = 0; + me.bottom = me.height; + } - // These methods are ordered by lifecycle. Utilities then follow. - // Any function defined here is inherited by all legend types. - // Any function can be extended by the legend type - - beforeUpdate: noop, - update: function(maxWidth, maxHeight, margins) { - var me = this; - - // Update Lifecycle - Probably don't want to ever extend or overwrite this function ;) - me.beforeUpdate(); - - // Absorb the master measurements - me.maxWidth = maxWidth; - me.maxHeight = maxHeight; - me.margins = margins; - - // Dimensions - me.beforeSetDimensions(); - me.setDimensions(); - me.afterSetDimensions(); - // Labels - me.beforeBuildLabels(); - me.buildLabels(); - me.afterBuildLabels(); - - // Fit - me.beforeFit(); - me.fit(); - me.afterFit(); - // - me.afterUpdate(); - - return me.minSize; - }, - afterUpdate: noop, + // Reset padding + me.paddingLeft = 0; + me.paddingTop = 0; + me.paddingRight = 0; + me.paddingBottom = 0; + + // Reset minSize + me.minSize = { + width: 0, + height: 0 + }; + }, + afterSetDimensions: noop, - // + // - beforeSetDimensions: noop, - setDimensions: function() { - var me = this; - // Set the unconstrained dimension before label rotation - if (me.isHorizontal()) { - // Reset position before calculating rotation - me.width = me.maxWidth; - me.left = 0; - me.right = me.width; - } else { - me.height = me.maxHeight; + beforeBuildLabels: noop, + buildLabels: function() { + var me = this; + var labelOpts = me.options.labels || {}; + var legendItems = helpers.callback(labelOpts.generateLabels, [me.chart], me) || []; - // Reset position before calculating rotation - me.top = 0; - me.bottom = me.height; - } + if (labelOpts.filter) { + legendItems = legendItems.filter(function(item) { + return labelOpts.filter(item, me.chart.data); + }); + } - // Reset padding - me.paddingLeft = 0; - me.paddingTop = 0; - me.paddingRight = 0; - me.paddingBottom = 0; + if (me.options.reverse) { + legendItems.reverse(); + } - // Reset minSize - me.minSize = { - width: 0, - height: 0 - }; - }, - afterSetDimensions: noop, + me.legendItems = legendItems; + }, + afterBuildLabels: noop, + + // + + beforeFit: noop, + fit: function() { + var me = this; + var opts = me.options; + var labelOpts = opts.labels; + var display = opts.display; + + var ctx = me.ctx; + + var globalDefault = defaults.global; + var valueOrDefault = helpers.valueOrDefault; + var fontSize = valueOrDefault(labelOpts.fontSize, globalDefault.defaultFontSize); + var fontStyle = valueOrDefault(labelOpts.fontStyle, globalDefault.defaultFontStyle); + var fontFamily = valueOrDefault(labelOpts.fontFamily, globalDefault.defaultFontFamily); + var labelFont = helpers.fontString(fontSize, fontStyle, fontFamily); + + // Reset hit boxes + var hitboxes = me.legendHitBoxes = []; + + var minSize = me.minSize; + var isHorizontal = me.isHorizontal(); + + if (isHorizontal) { + minSize.width = me.maxWidth; // fill all the width + minSize.height = display ? 10 : 0; + } else { + minSize.width = display ? 10 : 0; + minSize.height = me.maxHeight; // fill all the height + } - // + // Increase sizes here + if (display) { + ctx.font = labelFont; - beforeBuildLabels: noop, - buildLabels: function() { - var me = this; - var labelOpts = me.options.labels || {}; - var legendItems = helpers.callback(labelOpts.generateLabels, [me.chart], me) || []; + if (isHorizontal) { + // Labels - if (labelOpts.filter) { - legendItems = legendItems.filter(function(item) { - return labelOpts.filter(item, me.chart.data); - }); - } + // Width of each line of legend boxes. Labels wrap onto multiple lines when there are too many to fit on one + var lineWidths = me.lineWidths = [0]; + var totalHeight = me.legendItems.length ? fontSize + (labelOpts.padding) : 0; - if (me.options.reverse) { - legendItems.reverse(); - } + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; - me.legendItems = legendItems; - }, - afterBuildLabels: noop, + helpers.each(me.legendItems, function(legendItem, i) { + 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); + lineWidths[lineWidths.length] = me.left; + } - beforeFit: noop, - fit: function() { - var me = this; - var opts = me.options; - var labelOpts = opts.labels; - var display = opts.display; + // Store the hitbox width and height here. Final position will be updated in `draw` + hitboxes[i] = { + left: 0, + top: 0, + width: width, + height: fontSize + }; - var ctx = me.ctx; + lineWidths[lineWidths.length - 1] += width + labelOpts.padding; + }); - var globalDefault = defaults.global; - var valueOrDefault = helpers.valueOrDefault; - var fontSize = valueOrDefault(labelOpts.fontSize, globalDefault.defaultFontSize); - var fontStyle = valueOrDefault(labelOpts.fontStyle, globalDefault.defaultFontStyle); - var fontFamily = valueOrDefault(labelOpts.fontFamily, globalDefault.defaultFontFamily); - var labelFont = helpers.fontString(fontSize, fontStyle, fontFamily); + minSize.height += totalHeight; - // Reset hit boxes - var hitboxes = me.legendHitBoxes = []; + } else { + var vPadding = labelOpts.padding; + var columnWidths = me.columnWidths = []; + var totalWidth = labelOpts.padding; + var currentColWidth = 0; + var currentColHeight = 0; + var itemHeight = fontSize + vPadding; - var minSize = me.minSize; - var isHorizontal = me.isHorizontal(); + helpers.each(me.legendItems, function(legendItem, i) { + var itemWidth = legendItem.boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; - if (isHorizontal) { - minSize.width = me.maxWidth; // fill all the width - minSize.height = display ? 10 : 0; - } else { - minSize.width = display ? 10 : 0; - minSize.height = me.maxHeight; // fill all the height - } + // If too tall, go to new column + if (currentColHeight + itemHeight > minSize.height) { + totalWidth += currentColWidth + labelOpts.padding; + columnWidths.push(currentColWidth); // previous column width - // Increase sizes here - if (display) { - ctx.font = labelFont; + currentColWidth = 0; + currentColHeight = 0; + } - if (isHorizontal) { - // Labels + // Get max width + currentColWidth = Math.max(currentColWidth, itemWidth); + currentColHeight += itemHeight; - // Width of each line of legend boxes. Labels wrap onto multiple lines when there are too many to fit on one - var lineWidths = me.lineWidths = [0]; - var totalHeight = me.legendItems.length ? fontSize + (labelOpts.padding) : 0; + // Store the hitbox width and height here. Final position will be updated in `draw` + hitboxes[i] = { + left: 0, + top: 0, + width: itemWidth, + height: fontSize + }; + }); - ctx.textAlign = 'left'; - ctx.textBaseline = 'top'; + totalWidth += currentColWidth; + columnWidths.push(currentColWidth); + minSize.width += totalWidth; + } + } - helpers.each(me.legendItems, function(legendItem, i) { - var width = legendItem.boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; + me.width = minSize.width; + me.height = minSize.height; + }, + afterFit: noop, - if (lineWidths[lineWidths.length - 1] + width + labelOpts.padding >= me.width) { - totalHeight += fontSize + (labelOpts.padding); - lineWidths[lineWidths.length] = me.left; - } + // Shared Methods + isHorizontal: function() { + return this.options.position === 'top' || this.options.position === 'bottom'; + }, - // Store the hitbox width and height here. Final position will be updated in `draw` - hitboxes[i] = { - left: 0, - top: 0, - width: width, - height: fontSize - }; + // Actually draw the legend on the canvas + draw: function() { + var me = this; + var opts = me.options; + var labelOpts = opts.labels; + var globalDefault = defaults.global; + var lineDefault = globalDefault.elements.line; + var legendWidth = me.width; + var lineWidths = me.lineWidths; + + if (opts.display) { + var ctx = me.ctx; + var valueOrDefault = helpers.valueOrDefault; + var fontColor = valueOrDefault(labelOpts.fontColor, globalDefault.defaultFontColor); + var fontSize = valueOrDefault(labelOpts.fontSize, globalDefault.defaultFontSize); + var fontStyle = valueOrDefault(labelOpts.fontStyle, globalDefault.defaultFontStyle); + var fontFamily = valueOrDefault(labelOpts.fontFamily, globalDefault.defaultFontFamily); + var labelFont = helpers.fontString(fontSize, fontStyle, fontFamily); + var cursor; - lineWidths[lineWidths.length - 1] += width + labelOpts.padding; - }); + // Canvas setup + ctx.textAlign = 'left'; + ctx.textBaseline = 'middle'; + ctx.lineWidth = 0.5; + ctx.strokeStyle = fontColor; // for strikethrough effect + ctx.fillStyle = fontColor; // render in correct colour + ctx.font = labelFont; - minSize.height += totalHeight; + var hitboxes = me.legendHitBoxes; - } else { - var vPadding = labelOpts.padding; - var columnWidths = me.columnWidths = []; - var totalWidth = labelOpts.padding; - var currentColWidth = 0; - var currentColHeight = 0; - var itemHeight = fontSize + vPadding; - - helpers.each(me.legendItems, function(legendItem, i) { - var itemWidth = legendItem.boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; - - // If too tall, go to new column - if (currentColHeight + itemHeight > minSize.height) { - totalWidth += currentColWidth + labelOpts.padding; - columnWidths.push(currentColWidth); // previous column width - - currentColWidth = 0; - currentColHeight = 0; - } - - // Get max width - currentColWidth = Math.max(currentColWidth, itemWidth); - currentColHeight += itemHeight; - - // Store the hitbox width and height here. Final position will be updated in `draw` - hitboxes[i] = { - left: 0, - top: 0, - width: itemWidth, - height: fontSize - }; - }); - - totalWidth += currentColWidth; - columnWidths.push(currentColWidth); - minSize.width += totalWidth; + // current position + var drawLegendBox = function(x, y, legendItem) { + if (isNaN(legendItem.boxWidth) || legendItem.boxWidth <= 0) { + return; } - } - me.width = minSize.width; - me.height = minSize.height; - }, - afterFit: noop, + // Set the ctx for the box + ctx.save(); - // Shared Methods - isHorizontal: function() { - return this.options.position === 'top' || this.options.position === 'bottom'; - }, + ctx.fillStyle = valueOrDefault(legendItem.fillStyle, globalDefault.defaultColor); + ctx.lineCap = valueOrDefault(legendItem.lineCap, lineDefault.borderCapStyle); + ctx.lineDashOffset = valueOrDefault(legendItem.lineDashOffset, lineDefault.borderDashOffset); + ctx.lineJoin = valueOrDefault(legendItem.lineJoin, lineDefault.borderJoinStyle); + ctx.lineWidth = valueOrDefault(legendItem.lineWidth, lineDefault.borderWidth); + ctx.strokeStyle = valueOrDefault(legendItem.strokeStyle, globalDefault.defaultColor); + var isLineWidthZero = (valueOrDefault(legendItem.lineWidth, lineDefault.borderWidth) === 0); - // Actually draw the legend on the canvas - draw: function() { - var me = this; - var opts = me.options; - var labelOpts = opts.labels; - var globalDefault = defaults.global; - var lineDefault = globalDefault.elements.line; - var legendWidth = me.width; - var lineWidths = me.lineWidths; - - if (opts.display) { - var ctx = me.ctx; - var valueOrDefault = helpers.valueOrDefault; - var fontColor = valueOrDefault(labelOpts.fontColor, globalDefault.defaultFontColor); - var fontSize = valueOrDefault(labelOpts.fontSize, globalDefault.defaultFontSize); - var fontStyle = valueOrDefault(labelOpts.fontStyle, globalDefault.defaultFontStyle); - var fontFamily = valueOrDefault(labelOpts.fontFamily, globalDefault.defaultFontFamily); - var labelFont = helpers.fontString(fontSize, fontStyle, fontFamily); - var cursor; - - // Canvas setup - ctx.textAlign = 'left'; - ctx.textBaseline = 'middle'; - ctx.lineWidth = 0.5; - ctx.strokeStyle = fontColor; // for strikethrough effect - ctx.fillStyle = fontColor; // render in correct colour - ctx.font = labelFont; - - var hitboxes = me.legendHitBoxes; - - // current position - var drawLegendBox = function(x, y, legendItem) { - if (isNaN(legendItem.boxWidth) || legendItem.boxWidth <= 0) { - return; - } + if (ctx.setLineDash) { + // IE 9 and 10 do not support line dash + ctx.setLineDash(valueOrDefault(legendItem.lineDash, lineDefault.borderDash)); + } - // Set the ctx for the box - ctx.save(); + var strokeMe = helpers.canvas.drawSymbol(ctx, (labelOpts.usePointStyle ? legendItem.pointStyle : legendItem.legendSymbol), legendItem.boxWidth, fontSize, x, y); + if (!isLineWidthZero && strokeMe) { + ctx.stroke(); + } - ctx.fillStyle = valueOrDefault(legendItem.fillStyle, globalDefault.defaultColor); - ctx.lineCap = valueOrDefault(legendItem.lineCap, lineDefault.borderCapStyle); - ctx.lineDashOffset = valueOrDefault(legendItem.lineDashOffset, lineDefault.borderDashOffset); - ctx.lineJoin = valueOrDefault(legendItem.lineJoin, lineDefault.borderJoinStyle); - ctx.lineWidth = valueOrDefault(legendItem.lineWidth, lineDefault.borderWidth); - ctx.strokeStyle = valueOrDefault(legendItem.strokeStyle, globalDefault.defaultColor); - var isLineWidthZero = (valueOrDefault(legendItem.lineWidth, lineDefault.borderWidth) === 0); + ctx.restore(); + }; + var fillText = function(x, y, legendItem, textWidth) { + var halfFontSize = fontSize / 2; + var xLeft = legendItem.boxWidth + halfFontSize + x; + var yMiddle = y + halfFontSize; + + ctx.fillText(legendItem.text, xLeft, yMiddle); + + if (legendItem.hidden) { + // Strikethrough the text if hidden + ctx.beginPath(); + ctx.lineWidth = 2; + ctx.moveTo(xLeft, yMiddle); + ctx.lineTo(xLeft + textWidth, yMiddle); + ctx.stroke(); + } + }; - if (ctx.setLineDash) { - // IE 9 and 10 do not support line dash - ctx.setLineDash(valueOrDefault(legendItem.lineDash, lineDefault.borderDash)); - } - var strokeMe = helpers.canvas.drawSymbol(ctx, (labelOpts.usePointStyle ? legendItem.pointStyle : legendItem.legendSymbol), legendItem.boxWidth, fontSize, x, y); - if (!isLineWidthZero && strokeMe) { - ctx.stroke(); - } - ctx.restore(); + // Horizontal + var isHorizontal = me.isHorizontal(); + if (isHorizontal) { + cursor = { + x: me.left + ((legendWidth - lineWidths[0]) / 2), + y: me.top + labelOpts.padding, + line: 0 }; - var fillText = function(x, y, legendItem, textWidth) { - var halfFontSize = fontSize / 2; - var xLeft = legendItem.boxWidth + halfFontSize + x; - var yMiddle = y + halfFontSize; - - ctx.fillText(legendItem.text, xLeft, yMiddle); - - if (legendItem.hidden) { - // Strikethrough the text if hidden - ctx.beginPath(); - ctx.lineWidth = 2; - ctx.moveTo(xLeft, yMiddle); - ctx.lineTo(xLeft + textWidth, yMiddle); - ctx.stroke(); - } + } else { + cursor = { + x: me.left + labelOpts.padding, + y: me.top + labelOpts.padding, + line: 0 }; + } - // Horizontal - var isHorizontal = me.isHorizontal(); - if (isHorizontal) { - cursor = { - x: me.left + ((legendWidth - lineWidths[0]) / 2), - y: me.top + labelOpts.padding, - line: 0 - }; - } else { - cursor = { - x: me.left + labelOpts.padding, - y: me.top + labelOpts.padding, - line: 0 - }; - } + var itemHeight = fontSize + labelOpts.padding; + helpers.each(me.legendItems, function(legendItem, i) { + var textWidth = ctx.measureText(legendItem.text).width; + var width = legendItem.boxWidth + (fontSize / 2) + textWidth; + var x = cursor.x; + var y = cursor.y; - var itemHeight = fontSize + labelOpts.padding; - helpers.each(me.legendItems, function(legendItem, i) { - var textWidth = ctx.measureText(legendItem.text).width; - var width = legendItem.boxWidth + (fontSize / 2) + textWidth; - var x = cursor.x; - var y = cursor.y; - - if (isHorizontal) { - if (x + width >= legendWidth) { - y = cursor.y += itemHeight; - cursor.line++; - x = cursor.x = me.left + ((legendWidth - lineWidths[cursor.line]) / 2); - } - } else if (y + itemHeight > me.bottom) { - x = cursor.x = x + me.columnWidths[cursor.line] + labelOpts.padding; - y = cursor.y = me.top + labelOpts.padding; + if (isHorizontal) { + if (x + width >= legendWidth) { + y = cursor.y += itemHeight; cursor.line++; + x = cursor.x = me.left + ((legendWidth - lineWidths[cursor.line]) / 2); } + } else if (y + itemHeight > me.bottom) { + x = cursor.x = x + me.columnWidths[cursor.line] + labelOpts.padding; + y = cursor.y = me.top + labelOpts.padding; + cursor.line++; + } - drawLegendBox(x, y, legendItem); + drawLegendBox(x, y, legendItem); - hitboxes[i].left = x; - hitboxes[i].top = y; + hitboxes[i].left = x; + hitboxes[i].top = y; - // Fill the actual label - fillText(x, y, legendItem, textWidth); + // Fill the actual label + fillText(x, y, legendItem, textWidth); - if (isHorizontal) { - cursor.x += width + (labelOpts.padding); - } else { - cursor.y += itemHeight; - } + if (isHorizontal) { + cursor.x += width + (labelOpts.padding); + } else { + cursor.y += itemHeight; + } - }); - } - }, + }); + } + }, - /** - * Handle an event - * @private - * @param {IEvent} event - The event to handle - * @return {Boolean} true if a change occured - */ - handleEvent: function(e) { - var me = this; - var opts = me.options; - var type = e.type === 'mouseup' ? 'click' : e.type; - var changed = false; - - if (type === 'mousemove') { - if (!opts.onHover) { - return; - } - } else if (type === 'click') { - if (!opts.onClick) { - return; - } - } else { + /** + * Handle an event + * @private + * @param {IEvent} event - The event to handle + * @return {Boolean} true if a change occured + */ + handleEvent: function(e) { + var me = this; + var opts = me.options; + var type = e.type === 'mouseup' ? 'click' : e.type; + var changed = false; + + if (type === 'mousemove') { + if (!opts.onHover) { + return; + } + } else if (type === 'click') { + if (!opts.onClick) { return; } + } else { + return; + } - // Chart event already has relative position in it - var x = e.x; - var y = e.y; - - if (x >= me.left && x <= me.right && y >= me.top && y <= me.bottom) { - // See if we are touching one of the dataset boxes - var lh = me.legendHitBoxes; - for (var i = 0; i < lh.length; ++i) { - var hitBox = lh[i]; - - if (x >= hitBox.left && x <= hitBox.left + hitBox.width && y >= hitBox.top && y <= hitBox.top + hitBox.height) { - // Touching an element - if (type === 'click') { - // use e.native for backwards compatibility - opts.onClick.call(me, e.native, me.legendItems[i]); - changed = true; - break; - } else if (type === 'mousemove') { - // use e.native for backwards compatibility - opts.onHover.call(me, e.native, me.legendItems[i]); - changed = true; - break; - } + // Chart event already has relative position in it + var x = e.x; + var y = e.y; + + if (x >= me.left && x <= me.right && y >= me.top && y <= me.bottom) { + // See if we are touching one of the dataset boxes + var lh = me.legendHitBoxes; + for (var i = 0; i < lh.length; ++i) { + var hitBox = lh[i]; + + if (x >= hitBox.left && x <= hitBox.left + hitBox.width && y >= hitBox.top && y <= hitBox.top + hitBox.height) { + // Touching an element + if (type === 'click') { + // use e.native for backwards compatibility + opts.onClick.call(me, e.native, me.legendItems[i]); + changed = true; + break; + } else if (type === 'mousemove') { + // use e.native for backwards compatibility + opts.onHover.call(me, e.native, me.legendItems[i]); + changed = true; + break; } } } - - return changed; } - }); - function createNewLegendAndAttach(chart, legendOpts) { - var legend = new Chart.Legend({ - ctx: chart.ctx, - options: legendOpts, - chart: chart - }); - - layout.configure(chart, legend, legendOpts); - layout.addBox(chart, legend); - chart.legend = legend; + return changed; } +}); - return { - id: 'legend', +function createNewLegendAndAttach(chart, legendOpts) { + var legend = new Legend({ + ctx: chart.ctx, + options: legendOpts, + chart: chart + }); - beforeInit: function(chart) { - var legendOpts = chart.options.legend; + layouts.configure(chart, legend, legendOpts); + layouts.addBox(chart, legend); + chart.legend = legend; +} - if (legendOpts) { - createNewLegendAndAttach(chart, legendOpts); - } - }, +module.exports = { + id: 'legend', - beforeUpdate: function(chart) { - var legendOpts = chart.options.legend; - var legend = chart.legend; + /** + * Backward compatibility: since 2.1.5, the legend is registered as a plugin, making + * Chart.Legend obsolete. To avoid a breaking change, we export the Legend as part of + * the plugin, which one will be re-exposed in the chart.js file. + * https://github.com/chartjs/Chart.js/pull/2640 + * @private + */ + _element: Legend, - if (legendOpts) { - helpers.mergeIf(legendOpts, defaults.global.legend); + beforeInit: function(chart) { + var legendOpts = chart.options.legend; - if (legend) { - layout.configure(chart, legend, legendOpts); - legend.options = legendOpts; - } else { - createNewLegendAndAttach(chart, legendOpts); - } - } else if (legend) { - layout.removeBox(chart, legend); - delete chart.legend; - } - }, + if (legendOpts) { + createNewLegendAndAttach(chart, legendOpts); + } + }, + + beforeUpdate: function(chart) { + var legendOpts = chart.options.legend; + var legend = chart.legend; + + if (legendOpts) { + helpers.mergeIf(legendOpts, defaults.global.legend); - afterEvent: function(chart, e) { - var legend = chart.legend; if (legend) { - legend.handleEvent(e); + layouts.configure(chart, legend, legendOpts); + legend.options = legendOpts; + } else { + createNewLegendAndAttach(chart, legendOpts); } + } else if (legend) { + layouts.removeBox(chart, legend); + delete chart.legend; } - }; + }, + + afterEvent: function(chart, e) { + var legend = chart.legend; + if (legend) { + legend.handleEvent(e); + } + } }; diff --git a/src/plugins/plugin.title.js b/src/plugins/plugin.title.js index ebefed294ff..47588844d4c 100644 --- a/src/plugins/plugin.title.js +++ b/src/plugins/plugin.title.js @@ -3,6 +3,9 @@ var defaults = require('../core/core.defaults'); var Element = require('../core/core.element'); var helpers = require('../helpers/index'); +var layouts = require('../core/core.layouts'); + +var noop = helpers.noop; defaults._set('global', { title: { @@ -17,227 +20,233 @@ defaults._set('global', { } }); -module.exports = function(Chart) { - - var layout = Chart.layoutService; - var noop = helpers.noop; - - Chart.Title = Element.extend({ - initialize: function(config) { - var me = this; - helpers.extend(me, config); - - // Contains hit boxes for each dataset (in dataset order) - me.legendHitBoxes = []; - }, - - // These methods are ordered by lifecycle. Utilities then follow. - - beforeUpdate: noop, - update: function(maxWidth, maxHeight, margins) { - var me = this; - - // Update Lifecycle - Probably don't want to ever extend or overwrite this function ;) - me.beforeUpdate(); - - // Absorb the master measurements - me.maxWidth = maxWidth; - me.maxHeight = maxHeight; - me.margins = margins; - - // Dimensions - me.beforeSetDimensions(); - me.setDimensions(); - me.afterSetDimensions(); - // Labels - me.beforeBuildLabels(); - me.buildLabels(); - me.afterBuildLabels(); - - // Fit - me.beforeFit(); - me.fit(); - me.afterFit(); - // - me.afterUpdate(); - - return me.minSize; - - }, - afterUpdate: noop, - - // - - beforeSetDimensions: noop, - setDimensions: function() { - var me = this; - // Set the unconstrained dimension before label rotation - if (me.isHorizontal()) { - // Reset position before calculating rotation - me.width = me.maxWidth; - me.left = 0; - me.right = me.width; - } else { - me.height = me.maxHeight; - - // Reset position before calculating rotation - me.top = 0; - me.bottom = me.height; - } - - // Reset padding - me.paddingLeft = 0; - me.paddingTop = 0; - me.paddingRight = 0; - me.paddingBottom = 0; - - // Reset minSize - me.minSize = { - width: 0, - height: 0 - }; - }, - afterSetDimensions: noop, - +/** + * IMPORTANT: this class is exposed publicly as Chart.Legend, backward compatibility required! + */ +var Title = Element.extend({ + initialize: function(config) { + var me = this; + helpers.extend(me, config); + + // Contains hit boxes for each dataset (in dataset order) + me.legendHitBoxes = []; + }, + + // These methods are ordered by lifecycle. Utilities then follow. + + beforeUpdate: noop, + update: function(maxWidth, maxHeight, margins) { + var me = this; + + // Update Lifecycle - Probably don't want to ever extend or overwrite this function ;) + me.beforeUpdate(); + + // Absorb the master measurements + me.maxWidth = maxWidth; + me.maxHeight = maxHeight; + me.margins = margins; + + // Dimensions + me.beforeSetDimensions(); + me.setDimensions(); + me.afterSetDimensions(); + // Labels + me.beforeBuildLabels(); + me.buildLabels(); + me.afterBuildLabels(); + + // Fit + me.beforeFit(); + me.fit(); + me.afterFit(); // + me.afterUpdate(); + + return me.minSize; + + }, + afterUpdate: noop, + + // + + beforeSetDimensions: noop, + setDimensions: function() { + var me = this; + // Set the unconstrained dimension before label rotation + if (me.isHorizontal()) { + // Reset position before calculating rotation + me.width = me.maxWidth; + me.left = 0; + me.right = me.width; + } else { + me.height = me.maxHeight; + + // Reset position before calculating rotation + me.top = 0; + me.bottom = me.height; + } - beforeBuildLabels: noop, - buildLabels: noop, - afterBuildLabels: noop, - - // + // Reset padding + me.paddingLeft = 0; + me.paddingTop = 0; + me.paddingRight = 0; + me.paddingBottom = 0; + + // Reset minSize + me.minSize = { + width: 0, + height: 0 + }; + }, + afterSetDimensions: noop, + + // + + beforeBuildLabels: noop, + buildLabels: noop, + afterBuildLabels: noop, + + // + + beforeFit: noop, + fit: function() { + var me = this; + var valueOrDefault = helpers.valueOrDefault; + var opts = me.options; + var display = opts.display; + var fontSize = valueOrDefault(opts.fontSize, defaults.global.defaultFontSize); + var minSize = me.minSize; + var lineCount = helpers.isArray(opts.text) ? opts.text.length : 1; + var lineHeight = helpers.options.toLineHeight(opts.lineHeight, fontSize); + var textSize = display ? (lineCount * lineHeight) + (opts.padding * 2) : 0; + + if (me.isHorizontal()) { + minSize.width = me.maxWidth; // fill all the width + minSize.height = textSize; + } else { + minSize.width = textSize; + minSize.height = me.maxHeight; // fill all the height + } - beforeFit: noop, - fit: function() { - var me = this; - var valueOrDefault = helpers.valueOrDefault; - var opts = me.options; - var display = opts.display; - var fontSize = valueOrDefault(opts.fontSize, defaults.global.defaultFontSize); - var minSize = me.minSize; - var lineCount = helpers.isArray(opts.text) ? opts.text.length : 1; + me.width = minSize.width; + me.height = minSize.height; + + }, + afterFit: noop, + + // Shared Methods + isHorizontal: function() { + var pos = this.options.position; + return pos === 'top' || pos === 'bottom'; + }, + + // Actually draw the title block on the canvas + draw: function() { + var me = this; + var ctx = me.ctx; + var valueOrDefault = helpers.valueOrDefault; + var opts = me.options; + var globalDefaults = defaults.global; + + if (opts.display) { + var fontSize = valueOrDefault(opts.fontSize, globalDefaults.defaultFontSize); + var fontStyle = valueOrDefault(opts.fontStyle, globalDefaults.defaultFontStyle); + var fontFamily = valueOrDefault(opts.fontFamily, globalDefaults.defaultFontFamily); + var titleFont = helpers.fontString(fontSize, fontStyle, fontFamily); var lineHeight = helpers.options.toLineHeight(opts.lineHeight, fontSize); - var textSize = display ? (lineCount * lineHeight) + (opts.padding * 2) : 0; - + var offset = lineHeight / 2 + opts.padding; + var rotation = 0; + var top = me.top; + var left = me.left; + var bottom = me.bottom; + var right = me.right; + var maxWidth, titleX, titleY; + + ctx.fillStyle = valueOrDefault(opts.fontColor, globalDefaults.defaultFontColor); // render in correct colour + ctx.font = titleFont; + + // Horizontal if (me.isHorizontal()) { - minSize.width = me.maxWidth; // fill all the width - minSize.height = textSize; + titleX = left + ((right - left) / 2); // midpoint of the width + titleY = top + offset; + maxWidth = right - left; } else { - minSize.width = textSize; - minSize.height = me.maxHeight; // fill all the height + titleX = opts.position === 'left' ? left + offset : right - offset; + titleY = top + ((bottom - top) / 2); + maxWidth = bottom - top; + rotation = Math.PI * (opts.position === 'left' ? -0.5 : 0.5); } - me.width = minSize.width; - me.height = minSize.height; - - }, - afterFit: noop, - - // Shared Methods - isHorizontal: function() { - var pos = this.options.position; - return pos === 'top' || pos === 'bottom'; - }, - - // Actually draw the title block on the canvas - draw: function() { - var me = this; - var ctx = me.ctx; - var valueOrDefault = helpers.valueOrDefault; - var opts = me.options; - var globalDefaults = defaults.global; - - if (opts.display) { - var fontSize = valueOrDefault(opts.fontSize, globalDefaults.defaultFontSize); - var fontStyle = valueOrDefault(opts.fontStyle, globalDefaults.defaultFontStyle); - var fontFamily = valueOrDefault(opts.fontFamily, globalDefaults.defaultFontFamily); - var titleFont = helpers.fontString(fontSize, fontStyle, fontFamily); - var lineHeight = helpers.options.toLineHeight(opts.lineHeight, fontSize); - var offset = lineHeight / 2 + opts.padding; - var rotation = 0; - var top = me.top; - var left = me.left; - var bottom = me.bottom; - var right = me.right; - var maxWidth, titleX, titleY; - - ctx.fillStyle = valueOrDefault(opts.fontColor, globalDefaults.defaultFontColor); // render in correct colour - ctx.font = titleFont; - - // Horizontal - if (me.isHorizontal()) { - titleX = left + ((right - left) / 2); // midpoint of the width - titleY = top + offset; - maxWidth = right - left; - } else { - titleX = opts.position === 'left' ? left + offset : right - offset; - titleY = top + ((bottom - top) / 2); - maxWidth = bottom - top; - rotation = Math.PI * (opts.position === 'left' ? -0.5 : 0.5); + ctx.save(); + ctx.translate(titleX, titleY); + ctx.rotate(rotation); + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + + var text = opts.text; + if (helpers.isArray(text)) { + var y = 0; + for (var i = 0; i < text.length; ++i) { + ctx.fillText(text[i], 0, y, maxWidth); + y += lineHeight; } - - ctx.save(); - ctx.translate(titleX, titleY); - ctx.rotate(rotation); - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - - var text = opts.text; - if (helpers.isArray(text)) { - var y = 0; - for (var i = 0; i < text.length; ++i) { - ctx.fillText(text[i], 0, y, maxWidth); - y += lineHeight; - } - } else { - ctx.fillText(text, 0, 0, maxWidth); - } - - ctx.restore(); + } else { + ctx.fillText(text, 0, 0, maxWidth); } + + ctx.restore(); } + } +}); + +function createNewTitleBlockAndAttach(chart, titleOpts) { + var title = new Title({ + ctx: chart.ctx, + options: titleOpts, + chart: chart }); - function createNewTitleBlockAndAttach(chart, titleOpts) { - var title = new Chart.Title({ - ctx: chart.ctx, - options: titleOpts, - chart: chart - }); + layouts.configure(chart, title, titleOpts); + layouts.addBox(chart, title); + chart.titleBlock = title; +} - layout.configure(chart, title, titleOpts); - layout.addBox(chart, title); - chart.titleBlock = title; - } +module.exports = { + id: 'title', - return { - id: 'title', + /** + * Backward compatibility: since 2.1.5, the title is registered as a plugin, making + * Chart.Title obsolete. To avoid a breaking change, we export the Title as part of + * the plugin, which one will be re-exposed in the chart.js file. + * https://github.com/chartjs/Chart.js/pull/2640 + * @private + */ + _element: Title, - beforeInit: function(chart) { - var titleOpts = chart.options.title; + beforeInit: function(chart) { + var titleOpts = chart.options.title; - if (titleOpts) { - createNewTitleBlockAndAttach(chart, titleOpts); - } - }, + if (titleOpts) { + createNewTitleBlockAndAttach(chart, titleOpts); + } + }, - beforeUpdate: function(chart) { - var titleOpts = chart.options.title; - var titleBlock = chart.titleBlock; + beforeUpdate: function(chart) { + var titleOpts = chart.options.title; + var titleBlock = chart.titleBlock; - if (titleOpts) { - helpers.mergeIf(titleOpts, defaults.global.title); + if (titleOpts) { + helpers.mergeIf(titleOpts, defaults.global.title); - if (titleBlock) { - layout.configure(chart, titleBlock, titleOpts); - titleBlock.options = titleOpts; - } else { - createNewTitleBlockAndAttach(chart, titleOpts); - } - } else if (titleBlock) { - Chart.layoutService.removeBox(chart, titleBlock); - delete chart.titleBlock; + if (titleBlock) { + layouts.configure(chart, titleBlock, titleOpts); + titleBlock.options = titleOpts; + } else { + createNewTitleBlockAndAttach(chart, titleOpts); } + } else if (titleBlock) { + layouts.removeBox(chart, titleBlock); + delete chart.titleBlock; } - }; + } }; diff --git a/src/scales/scale.linear.js b/src/scales/scale.linear.js index 94ebd12a5a0..aa723004fb7 100644 --- a/src/scales/scale.linear.js +++ b/src/scales/scale.linear.js @@ -170,11 +170,10 @@ module.exports = function(Chart) { if (me.isHorizontal()) { pixel = me.left + (me.width / range * (rightValue - start)); - return Math.round(pixel); + } else { + pixel = me.bottom - (me.height / range * (rightValue - start)); } - - pixel = me.bottom - (me.height / range * (rightValue - start)); - return Math.round(pixel); + return pixel; }, getValueForPixel: function(pixel) { var me = this; diff --git a/src/scales/scale.linearbase.js b/src/scales/scale.linearbase.js index 0d9a33bf2a3..3e4b5c083f3 100644 --- a/src/scales/scale.linearbase.js +++ b/src/scales/scale.linearbase.js @@ -1,7 +1,61 @@ 'use strict'; var helpers = require('../helpers/index'); -var Ticks = require('../core/core.ticks'); + +/** + * Generate a set of linear ticks + * @param generationOptions the options used to generate the ticks + * @param dataRange the range of the data + * @returns {Array} array of tick values + */ +function generateTicks(generationOptions, dataRange) { + var ticks = []; + // To get a "nice" value for the tick spacing, we will use the appropriately named + // "nice number" algorithm. See http://stackoverflow.com/questions/8506881/nice-label-algorithm-for-charts-with-minimum-ticks + // for details. + + var spacing; + if (generationOptions.stepSize && generationOptions.stepSize > 0) { + spacing = generationOptions.stepSize; + } else { + var niceRange = helpers.niceNum(dataRange.max - dataRange.min, false); + spacing = helpers.niceNum(niceRange / (generationOptions.maxTicks - 1), true); + } + var niceMin = Math.floor(dataRange.min / spacing) * spacing; + var niceMax = Math.ceil(dataRange.max / spacing) * spacing; + + // If min, max and stepSize is set and they make an evenly spaced scale use it. + if (generationOptions.min && generationOptions.max && generationOptions.stepSize) { + // If very close to our whole number, use it. + if (helpers.almostWhole((generationOptions.max - generationOptions.min) / generationOptions.stepSize, spacing / 1000)) { + niceMin = generationOptions.min; + niceMax = generationOptions.max; + } + } + + var numSpaces = (niceMax - niceMin) / spacing; + // If very close to our rounded value, use it. + if (helpers.almostEquals(numSpaces, Math.round(numSpaces), spacing / 1000)) { + numSpaces = Math.round(numSpaces); + } else { + numSpaces = Math.ceil(numSpaces); + } + + var precision = 1; + if (spacing < 1) { + precision = Math.pow(10, spacing.toString().length - 2); + niceMin = Math.round(niceMin * precision) / precision; + niceMax = Math.round(niceMax * precision) / precision; + } + ticks.push(generationOptions.min !== undefined ? generationOptions.min : niceMin); + for (var j = 1; j < numSpaces; ++j) { + ticks.push(Math.round((niceMin + j * spacing) * precision) / precision); + } + ticks.push(generationOptions.max !== undefined ? generationOptions.max : niceMax); + + return ticks; +} + module.exports = function(Chart) { @@ -102,7 +156,7 @@ module.exports = function(Chart) { max: tickOpts.max, stepSize: helpers.valueOrDefault(tickOpts.fixedStepSize, tickOpts.stepSize) }; - var ticks = me.ticks = Ticks.generators.linear(numericGeneratorOptions, me); + var ticks = me.ticks = generateTicks(numericGeneratorOptions, me); me.handleDirectionalChanges(); diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js index 78aaf8171c4..74a210e4473 100644 --- a/src/scales/scale.logarithmic.js +++ b/src/scales/scale.logarithmic.js @@ -3,6 +3,58 @@ var helpers = require('../helpers/index'); var Ticks = require('../core/core.ticks'); +/** + * Generate a set of logarithmic ticks + * @param generationOptions the options used to generate the ticks + * @param dataRange the range of the data + * @returns {Array} array of tick values + */ +function generateTicks(generationOptions, dataRange) { + var ticks = []; + var valueOrDefault = helpers.valueOrDefault; + + // Figure out what the max number of ticks we can support it is based on the size of + // the axis area. For now, we say that the minimum tick spacing in pixels must be 50 + // We also limit the maximum number of ticks to 11 which gives a nice 10 squares on + // the graph + var tickVal = valueOrDefault(generationOptions.min, Math.pow(10, Math.floor(helpers.log10(dataRange.min)))); + + var endExp = Math.floor(helpers.log10(dataRange.max)); + var endSignificand = Math.ceil(dataRange.max / Math.pow(10, endExp)); + var exp, significand; + + if (tickVal === 0) { + exp = Math.floor(helpers.log10(dataRange.minNotZero)); + significand = Math.floor(dataRange.minNotZero / Math.pow(10, exp)); + + ticks.push(tickVal); + tickVal = significand * Math.pow(10, exp); + } else { + exp = Math.floor(helpers.log10(tickVal)); + significand = Math.floor(tickVal / Math.pow(10, exp)); + } + var precision = exp < 0 ? Math.pow(10, Math.abs(exp)) : 1; + + do { + ticks.push(tickVal); + + ++significand; + if (significand === 10) { + significand = 1; + ++exp; + precision = exp >= 0 ? 1 : precision; + } + + tickVal = Math.round(significand * Math.pow(10, exp) * precision) / precision; + } while (exp < endExp || (exp === endExp && significand < endSignificand)); + + var lastTick = valueOrDefault(generationOptions.max, tickVal); + ticks.push(lastTick); + + return ticks; +} + + module.exports = function(Chart) { var defaultConfig = { @@ -18,11 +70,9 @@ module.exports = function(Chart) { determineDataLimits: function() { var me = this; var opts = me.options; - var tickOpts = opts.ticks; var chart = me.chart; var data = chart.data; var datasets = data.datasets; - var valueOrDefault = helpers.valueOrDefault; var isHorizontal = me.isHorizontal(); function IDMatches(meta) { return isHorizontal ? meta.xAxisID === me.id : meta.yAxisID === me.id; @@ -68,27 +118,23 @@ module.exports = function(Chart) { helpers.each(dataset.data, function(rawValue, index) { var values = valuesPerStack[key]; var value = +me.getRightValue(rawValue); - if (isNaN(value) || meta.data[index].hidden) { + // invalid, hidden and negative values are ignored + if (isNaN(value) || meta.data[index].hidden || value < 0) { return; } - values[index] = values[index] || 0; - - if (opts.relativePoints) { - values[index] = 100; - } else { - // Don't need to split positive and negative since the log scale can't handle a 0 crossing - values[index] += value; - } + values[index] += value; }); } }); helpers.each(valuesPerStack, function(valuesForType) { - var minVal = helpers.min(valuesForType); - var maxVal = helpers.max(valuesForType); - me.min = me.min === null ? minVal : Math.min(me.min, minVal); - me.max = me.max === null ? maxVal : Math.max(me.max, maxVal); + if (valuesForType.length > 0) { + var minVal = helpers.min(valuesForType); + var maxVal = helpers.max(valuesForType); + me.min = me.min === null ? minVal : Math.min(me.min, minVal); + me.max = me.max === null ? maxVal : Math.max(me.max, maxVal); + } }); } else { @@ -97,7 +143,8 @@ module.exports = function(Chart) { if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta)) { helpers.each(dataset.data, function(rawValue, index) { var value = +me.getRightValue(rawValue); - if (isNaN(value) || meta.data[index].hidden) { + // invalid, hidden and negative values are ignored + if (isNaN(value) || meta.data[index].hidden || value < 0) { return; } @@ -121,6 +168,17 @@ module.exports = function(Chart) { }); } + // Common base implementation to handle ticks.min, ticks.max + this.handleTickRangeOptions(); + }, + handleTickRangeOptions: function() { + var me = this; + var opts = me.options; + var tickOpts = opts.ticks; + var valueOrDefault = helpers.valueOrDefault; + var DEFAULT_MIN = 1; + var DEFAULT_MAX = 10; + me.min = valueOrDefault(tickOpts.min, me.min); me.max = valueOrDefault(tickOpts.max, me.max); @@ -129,8 +187,25 @@ module.exports = function(Chart) { me.min = Math.pow(10, Math.floor(helpers.log10(me.min)) - 1); me.max = Math.pow(10, Math.floor(helpers.log10(me.max)) + 1); } else { - me.min = 1; - me.max = 10; + me.min = DEFAULT_MIN; + me.max = DEFAULT_MAX; + } + } + if (me.min === null) { + me.min = Math.pow(10, Math.floor(helpers.log10(me.max)) - 1); + } + if (me.max === null) { + me.max = me.min !== 0 + ? Math.pow(10, Math.floor(helpers.log10(me.min)) + 1) + : DEFAULT_MAX; + } + if (me.minNotZero === null) { + if (me.min > 0) { + me.minNotZero = me.min; + } else if (me.max < 1) { + me.minNotZero = Math.pow(10, Math.floor(helpers.log10(me.max))); + } else { + me.minNotZero = DEFAULT_MIN; } } }, @@ -138,17 +213,13 @@ module.exports = function(Chart) { var me = this; var opts = me.options; var tickOpts = opts.ticks; + var reverse = !me.isHorizontal(); var generationOptions = { min: tickOpts.min, max: tickOpts.max }; - var ticks = me.ticks = Ticks.generators.logarithmic(generationOptions, me); - - if (!me.isHorizontal()) { - // We are in a vertical orientation. The top value is the highest. So reverse the array - ticks.reverse(); - } + var ticks = me.ticks = generateTicks(generationOptions, me); // At this point, we need to update our max and min given the tick values since we have expanded the // range of the scale @@ -156,14 +227,16 @@ module.exports = function(Chart) { me.min = helpers.min(ticks); if (tickOpts.reverse) { - ticks.reverse(); - + reverse = !reverse; me.start = me.max; me.end = me.min; } else { me.start = me.min; me.end = me.max; } + if (reverse) { + ticks.reverse(); + } }, convertTicksToLabels: function() { this.tickValues = this.ticks.slice(); @@ -177,64 +250,94 @@ module.exports = function(Chart) { getPixelForTick: function(index) { return this.getPixelForValue(this.tickValues[index]); }, + /** + * Returns the value of the first tick. + * @param {Number} value - The minimum not zero value. + * @return {Number} The first tick value. + * @private + */ + _getFirstTickValue: function(value) { + var exp = Math.floor(helpers.log10(value)); + var significand = Math.floor(value / Math.pow(10, exp)); + + return significand * Math.pow(10, exp); + }, getPixelForValue: function(value) { var me = this; - var start = me.start; - var newVal = +me.getRightValue(value); - var opts = me.options; - var tickOpts = opts.ticks; - var innerDimension, pixel, range; + var reverse = me.options.ticks.reverse; + var log10 = helpers.log10; + var firstTickValue = me._getFirstTickValue(me.minNotZero); + var offset = 0; + var innerDimension, pixel, start, end, sign; + value = +me.getRightValue(value); + if (reverse) { + start = me.end; + end = me.start; + sign = -1; + } else { + start = me.start; + end = me.end; + sign = 1; + } if (me.isHorizontal()) { - range = helpers.log10(me.end) - helpers.log10(start); // todo: if start === 0 - if (newVal === 0) { - pixel = me.left; - } else { - innerDimension = me.width; - pixel = me.left + (innerDimension / range * (helpers.log10(newVal) - helpers.log10(start))); - } + innerDimension = me.width; + pixel = reverse ? me.right : me.left; } else { - // Bottom - top since pixels increase downward on a screen innerDimension = me.height; - if (start === 0 && !tickOpts.reverse) { - range = helpers.log10(me.end) - helpers.log10(me.minNotZero); - if (newVal === start) { - pixel = me.bottom; - } else if (newVal === me.minNotZero) { - pixel = me.bottom - innerDimension * 0.02; - } else { - pixel = me.bottom - innerDimension * 0.02 - (innerDimension * 0.98 / range * (helpers.log10(newVal) - helpers.log10(me.minNotZero))); - } - } else if (me.end === 0 && tickOpts.reverse) { - range = helpers.log10(me.start) - helpers.log10(me.minNotZero); - if (newVal === me.end) { - pixel = me.top; - } else if (newVal === me.minNotZero) { - pixel = me.top + innerDimension * 0.02; - } else { - pixel = me.top + innerDimension * 0.02 + (innerDimension * 0.98 / range * (helpers.log10(newVal) - helpers.log10(me.minNotZero))); - } - } else if (newVal === 0) { - pixel = tickOpts.reverse ? me.top : me.bottom; - } else { - range = helpers.log10(me.end) - helpers.log10(start); - innerDimension = me.height; - pixel = me.bottom - (innerDimension / range * (helpers.log10(newVal) - helpers.log10(start))); + sign *= -1; // invert, since the upper-left corner of the canvas is at pixel (0, 0) + pixel = reverse ? me.top : me.bottom; + } + if (value !== start) { + if (start === 0) { // include zero tick + offset = helpers.getValueOrDefault( + me.options.ticks.fontSize, + Chart.defaults.global.defaultFontSize + ); + innerDimension -= offset; + start = firstTickValue; } + if (value !== 0) { + offset += innerDimension / (log10(end) - log10(start)) * (log10(value) - log10(start)); + } + pixel += sign * offset; } return pixel; }, getValueForPixel: function(pixel) { var me = this; - var range = helpers.log10(me.end) - helpers.log10(me.start); - var value, innerDimension; + var reverse = me.options.ticks.reverse; + var log10 = helpers.log10; + var firstTickValue = me._getFirstTickValue(me.minNotZero); + var innerDimension, start, end, value; + if (reverse) { + start = me.end; + end = me.start; + } else { + start = me.start; + end = me.end; + } if (me.isHorizontal()) { innerDimension = me.width; - value = me.start * Math.pow(10, (pixel - me.left) * range / innerDimension); - } else { // todo: if start === 0 + value = reverse ? me.right - pixel : pixel - me.left; + } else { innerDimension = me.height; - value = Math.pow(10, (me.bottom - pixel) * range / innerDimension) / me.start; + value = reverse ? pixel - me.top : me.bottom - pixel; + } + if (value !== start) { + if (start === 0) { // include zero tick + var offset = helpers.getValueOrDefault( + me.options.ticks.fontSize, + Chart.defaults.global.defaultFontSize + ); + value -= offset; + innerDimension -= offset; + start = firstTickValue; + } + value *= log10(end) - log10(start); + value /= innerDimension; + value = Math.pow(10, log10(start) + value); } return value; } diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index a87920c6781..4892eea9996 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -240,7 +240,7 @@ function determineStepSize(min, max, unit, capacity) { var i, ilen, factor; if (!steps) { - return Math.ceil(range / ((capacity || 1) * milliseconds)); + return Math.ceil(range / (capacity * milliseconds)); } for (i = 0, ilen = steps.length; i < ilen; ++i) { @@ -253,7 +253,10 @@ function determineStepSize(min, max, unit, capacity) { return factor; } -function determineUnit(minUnit, min, max, capacity) { +/** + * Figures out what unit results in an appropriate number of auto-generated ticks + */ +function determineUnitForAutoTicks(minUnit, min, max, capacity) { var ilen = UNITS.length; var i, interval, factor; @@ -269,6 +272,24 @@ function determineUnit(minUnit, min, max, capacity) { return UNITS[ilen - 1]; } +/** + * Figures out what unit to format a set of ticks with + */ +function determineUnitForFormatting(ticks, minUnit, min, max) { + var duration = moment.duration(moment(max).diff(moment(min))); + var ilen = UNITS.length; + var i, unit; + + for (i = ilen - 1; i >= UNITS.indexOf(minUnit); i--) { + unit = UNITS[i]; + if (INTERVALS[unit].common && duration.as(unit) >= ticks.length) { + return unit; + } + } + + return UNITS[minUnit ? UNITS.indexOf(minUnit) : 0]; +} + function determineMajorUnit(unit) { for (var i = UNITS.indexOf(unit) + 1, ilen = UNITS.length; i < ilen; ++i) { if (INTERVALS[UNITS[i]].common) { @@ -283,8 +304,10 @@ function determineMajorUnit(unit) { * Important: this method can return ticks outside the min and max range, it's the * responsibility of the calling code to clamp values if needed. */ -function generate(min, max, minor, major, capacity, options) { +function generate(min, max, capacity, options) { var timeOpts = options.time; + var minor = timeOpts.unit || determineUnitForAutoTicks(timeOpts.minUnit, min, max, capacity); + var major = determineMajorUnit(minor); var stepSize = helpers.valueOrDefault(timeOpts.stepSize, timeOpts.unitStepSize); var weekday = minor === 'week' ? timeOpts.isoWeekday : false; var majorTicksEnabled = options.ticks.major.enabled; @@ -380,6 +403,27 @@ function ticksFromTimestamps(values, majorUnit) { return ticks; } +function determineLabelFormat(data, timeOpts) { + var i, momentDate, hasTime; + var ilen = data.length; + + // find the label with the most parts (milliseconds, minutes, etc.) + // format all labels with the same level of detail as the most specific label + for (i = 0; i < ilen; i++) { + momentDate = momentify(data[i], timeOpts); + if (momentDate.millisecond() !== 0) { + return 'MMM D, YYYY h:mm:ss.SSS a'; + } + if (momentDate.second() !== 0 || momentDate.minute() !== 0 || momentDate.hour() !== 0) { + hasTime = true; + } + } + if (hasTime) { + return 'MMM D, YYYY h:mm:ss a'; + } + return 'MMM D, YYYY'; +} + module.exports = function(Chart) { var defaultConfig = { @@ -481,8 +525,9 @@ module.exports = function(Chart) { var me = this; var chart = me.chart; var timeOpts = me.options.time; - var min = parse(timeOpts.min, me) || MAX_INTEGER; - var max = parse(timeOpts.max, me) || MIN_INTEGER; + var unit = timeOpts.unit || 'day'; + var min = MAX_INTEGER; + var max = MIN_INTEGER; var timestamps = []; var datasets = []; var labels = []; @@ -529,9 +574,12 @@ module.exports = function(Chart) { max = Math.max(max, timestamps[timestamps.length - 1]); } - // In case there is no valid min/max, let's use today limits - min = min === MAX_INTEGER ? +moment().startOf('day') : min; - max = max === MIN_INTEGER ? +moment().endOf('day') + 1 : max; + min = parse(timeOpts.min, me) || min; + max = parse(timeOpts.max, me) || max; + + // In case there is no valid min/max, set limits based on unit time option + min = min === MAX_INTEGER ? +moment().startOf(unit) : min; + max = max === MIN_INTEGER ? +moment().endOf(unit) + 1 : max; // Make sure that max is strictly higher than min (required by the lookup table) me.min = Math.min(min, max); @@ -553,10 +601,6 @@ module.exports = function(Chart) { var max = me.max; var options = me.options; var timeOpts = options.time; - var formats = timeOpts.displayFormats; - var capacity = me.getLabelCapacity(min); - var unit = timeOpts.unit || determineUnit(timeOpts.minUnit, min, max, capacity); - var majorUnit = determineMajorUnit(unit); var timestamps = []; var ticks = []; var i, ilen, timestamp; @@ -570,7 +614,7 @@ module.exports = function(Chart) { break; case 'auto': default: - timestamps = generate(min, max, unit, majorUnit, capacity, options); + timestamps = generate(min, max, me.getLabelCapacity(min), options); } if (options.bounds === 'ticks' && timestamps.length) { @@ -594,14 +638,13 @@ module.exports = function(Chart) { me.max = max; // PRIVATE - me._unit = unit; - me._majorUnit = majorUnit; - me._minorFormat = formats[unit]; - me._majorFormat = formats[majorUnit]; + me._unit = timeOpts.unit || determineUnitForFormatting(ticks, timeOpts.minUnit, me.min, me.max); + me._majorUnit = determineMajorUnit(me._unit); me._table = buildLookupTable(me._timestamps.data, min, max, options.distribution); me._offsets = computeOffsets(me._table, ticks, min, max, options); + me._labelFormat = determineLabelFormat(me._timestamps.data, timeOpts); - return ticksFromTimestamps(ticks, majorUnit); + return ticksFromTimestamps(ticks, me._majorUnit); }, getLabelForIndex: function(index, datasetIndex) { @@ -615,26 +658,31 @@ module.exports = function(Chart) { label = me.getRightValue(value); } if (timeOpts.tooltipFormat) { - label = momentify(label, timeOpts).format(timeOpts.tooltipFormat); + return momentify(label, timeOpts).format(timeOpts.tooltipFormat); + } + if (typeof label === 'string') { + return label; } - return label; + return momentify(label, timeOpts).format(me._labelFormat); }, /** * Function to format an individual tick mark * @private */ - tickFormatFunction: function(tick, index, ticks) { + tickFormatFunction: function(tick, index, ticks, formatOverride) { var me = this; var options = me.options; var time = tick.valueOf(); + var formats = options.time.displayFormats; + var minorFormat = formats[me._unit]; var majorUnit = me._majorUnit; - var majorFormat = me._majorFormat; - var majorTime = tick.clone().startOf(me._majorUnit).valueOf(); + var majorFormat = formats[majorUnit]; + var majorTime = tick.clone().startOf(majorUnit).valueOf(); var majorTickOpts = options.ticks.major; var major = majorTickOpts.enabled && majorUnit && majorFormat && time === majorTime; - var label = tick.format(major ? majorFormat : me._minorFormat); + var label = tick.format(formatOverride ? formatOverride : major ? majorFormat : minorFormat); var tickOpts = major ? majorTickOpts : options.ticks.minor; var formatter = helpers.valueOrDefault(tickOpts.callback, tickOpts.userCallback); @@ -720,13 +768,14 @@ module.exports = function(Chart) { getLabelCapacity: function(exampleTime) { var me = this; - me._minorFormat = me.options.time.displayFormats.millisecond; // Pick the longest format for guestimation + var formatOverride = me.options.time.displayFormats.millisecond; // Pick the longest format for guestimation - var exampleLabel = me.tickFormatFunction(moment(exampleTime), 0, []); + var exampleLabel = me.tickFormatFunction(moment(exampleTime), 0, [], formatOverride); var tickLabelWidth = me.getLabelWidth(exampleLabel); var innerWidth = me.isHorizontal() ? me.width : me.height; - return Math.floor(innerWidth / tickLabelWidth); + var capacity = Math.floor(innerWidth / tickLabelWidth); + return capacity > 0 ? capacity : 1; } }); diff --git a/test/.eslintrc b/test/.eslintrc.yml similarity index 89% rename from test/.eslintrc rename to test/.eslintrc.yml index 8e8f899bffd..9d98c452878 100644 --- a/test/.eslintrc +++ b/test/.eslintrc.yml @@ -11,3 +11,4 @@ globals: rules: # Best Practices complexity: 0 + max-statements: 0 diff --git a/test/fixtures/controller.bar/bar-thickness-absolute.json b/test/fixtures/controller.bar/bar-thickness-absolute.json new file mode 100644 index 00000000000..599b090d6d1 --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-absolute.json @@ -0,0 +1,42 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2017", "2018", "2019", "2024", "2025"], + "datasets": [{ + "backgroundColor": "rgba(255, 99, 132, 0.5)", + "data": [1, null, 3, 4, 5] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "time", + "offset": true, + "display": false, + "barPercentage": 1, + "categoryPercentage": 1, + "barThickness": 128, + "ticks": { + "source": "labels" + } + }], + "yAxes": [{ + "display": false, + "ticks": { + "beginAtZero": true + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-absolute.png b/test/fixtures/controller.bar/bar-thickness-absolute.png new file mode 100644 index 0000000000000000000000000000000000000000..40172b39241f2a6e13d201f7a47509137acc468e GIT binary patch literal 5055 zcmeHLe^gU-8-MQQA{Pi8>=>pT*g%F!uR}A%6F+XTcSFf(?|F;fw^xU!Q)HUUOg4nU zm$D{epsxp)S!~)lQc$2$NV3L`l|UHlgi1Na29$^}_XBYZ*w%N}@f^!PJN?uCxaZ#I zJkR&@e7?_lew|yfK`e?0TMz~S5o>Y;{{kTKCjs;v{QCMpPXPe?tu=zj{w?2RHD*-3 zTrh9XV6On}+1nL+73+n`aTzjy^9*HdyPx`SC;)5HD z={wm`ezzq=nl^~C*y)FEyRw8cx}ZCb2nj^mfJjRx!*4U0A`f7(^Wjxd7?;t<1l$IK z{AZ?=d|r9Hx7!h;)?HWjxRhC6m@;R*C0j#QmX=PBSV!G$#p@Hm#k=m};$)TaQXTD8 zYN@)vcDuU&0n;O;ritD&lx4w_yMz5jvxKB9Pqj9>l%e{`<&z0GVJ%4%tWs|v6k?IHDIupoPKSQA8i*#(-Pa^e7MC;b5e5=ToBsk z+;tQy7y6Ya*CP0s=T`rl^`P4u{TOZ!hCQb;q`I)e_V`HQj8q-tx5fc}Co~x~!wOx^~4+RjEEF z@in$!H4a;n5921q zNS3G_<8wgvb&*aYgdWw86{~oV9BGSdV58ybZHE+Bxkf%@ehkF11Ma7jVWfM!aUmPE z4VAd~iC`1TZ%7#EdEeaCk_^e=;{#*SsFC_0)J zfep#tj8B5R9LEYN2kI)WFRP439j_?Hq)8x6k{whhLQJSL*1&<0&e6@!MkD1hMK7NR z(sPCH$0tBvmy_aiA(*pxl?df8$4U+Tqy)WgxXgsoYw1H-}q<-`n&Fs6`eU*h&JI;=)6F~KLi(@mTSyp2aQyh9G_ybxW&e`a$ zW}%e2XI(4F4KZs9x#WSLJ*T!%mgn{?^{UyZGNgYpo&2swRI-p{iM-80p-f(!<7JwO zbvqgIQci$wH0kLq--#bN*aT<*&vxRGGww zm5bU~M5i-OEriSAmL{lQz(D;_CqEl6JIdr}G-&tCV(LXJ7D#I+^xyBo@a?!j=*A1`Q*2oMZJ@Ykdc zoVE3Y?(+Zf QFFJrV{}c-vAJ2dN7yOH(lK=n! literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.bar/bar-thickness-flex-offset.json b/test/fixtures/controller.bar/bar-thickness-flex-offset.json new file mode 100644 index 00000000000..1776b07be16 --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-flex-offset.json @@ -0,0 +1,42 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2017", "2018", "2020", "2024", "2038"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [1, null, 3, 4, 5] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "time", + "offset": true, + "display": false, + "barPercentage": 1, + "categoryPercentage": 1, + "barThickness": "flex", + "ticks": { + "source": "labels" + } + }], + "yAxes": [{ + "display": false, + "ticks": { + "beginAtZero": true + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-flex-offset.png b/test/fixtures/controller.bar/bar-thickness-flex-offset.png new file mode 100644 index 0000000000000000000000000000000000000000..e20cc4eb450c8f90b94da145252619ba60cec024 GIT binary patch literal 4583 zcmeHK>r)d~6hC{j;0A-)Xtj!nOAwV7LA2JOJQfI`#p3(XN`)#dK1oEyAWC*)%R_C{ zX~$@dQroe9utht>X2%I@XKM#PQNCD%Izl(dEc>tQKnfjy!re&S2G0tx`jY@xf;Lg5h ziy|8?{d8f}X(nc^n5?a6xNyVo^736ZpG?nDSG(R_d3l|&Ew{9+tJ|)gaQ?@xVrRYX z%ouk`x0*1VE0Gu~`|h1^w6xq8I6>-Bww_45o=mf*Xo_+zo0m`~^(q;mdwMm9HVq5W zDSzx__mAV?_o1<@*ugMD#}ZFCU6N0TeE&_^tr@Q6pfp$KCsytk;CTh**+M(@JlL=7 zTdFZx1u$7F`-8*`G-bQ7N=5o@0=RgVmj@6ct>C{%;bCLe(j8I?+6&c==H*E=Eq$;c z$Rc&4(5>30AbFu2IG0~N#02v^1jFsw+{9!ac9k3b$w1Vx_)X=EDvC0y^6D*81V%W2 zWx*H{jk}gvuG=_cAes&e`IeXCQM-72uZn00;k3WZqVpXG!KM0-1oI*u+JG=yXYpWC zGaH%bLj>!Z42@|94{`dt3MQ*mfYZC)N?}ch5!J&-47?nP3B5Dx%n(t5St~e?l1@WG zqjB;%q5w-y%Ko$!VX*%8a51a%Lx=+2w(8|@+?6r$ufcRx5)WDDzZ%Nam7zs-cx4iY z!0Pn}D}?$1(nub}-AV?!C~**FCe7>DMG!SoqpW-@h)pckbM7jJTpPl|waHHd8T-cq z=ypbI)*1mFL2jZbF%&gXdu{|1iOK_qwF*)UdXBMs1IRIsgUlmW_A+%Jp=V`UwndtP zk*_gT6VZ*@T}SRtX7spHzH+k0P$Vq7SIrqmV1XyS z7MLU#p-@!%10|k98+kGJGU-9+Xpr!596KCwNYm>Sq8}kS4VICt4&l_5p2|*=U%{v% z-iRG6+HhBN!-EX=v_Ga0xO)-j3PFqUpKbLQF&9>|ZY=2w0RvEU!dMRK2GG;!7fQad z<|q?4hJz686N-*PLT9BN4Q1kHW8JPbBR7YkjmWQCD+US{-28L>qaAoWXs&;#HP$MHLyI{m%-R^Of%n||?qd^a6pD}wchMGUe$I$ytn>iO= zJ%wW;Un(6Za*O9y_KQ2rY9cdNOTsdE$h|qTEer>n-IEQ|qt4nGp{lrP1723G1=Puo zFN-VS!C6OKIu8%Wzv@%);qd?BgS?P3)?zn#nJtFOZM|1I@rD@ucxj1345p|(^Ye{1 Z$qf1fHLEYRwyyyEW~OH9tCDjpe*@UW)UW^m literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.bar/bar-thickness-flex.json b/test/fixtures/controller.bar/bar-thickness-flex.json new file mode 100644 index 00000000000..0bef9db1828 --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-flex.json @@ -0,0 +1,41 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2017", "2018", "2020", "2024", "2038"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [1, null, 3, 4, 5] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "time", + "display": false, + "barPercentage": 1, + "categoryPercentage": 1, + "barThickness": "flex", + "ticks": { + "source": "labels" + } + }], + "yAxes": [{ + "display": false, + "ticks": { + "beginAtZero": true + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-flex.png b/test/fixtures/controller.bar/bar-thickness-flex.png new file mode 100644 index 0000000000000000000000000000000000000000..791a29d25d380f64f8c33fc3a928cebc531348cb GIT binary patch literal 5095 zcmeHLZB!Fy7QQn~U;;=d1Q4o_k?@hO;HG}CAW#QI0*D~h^{XhMrAp6g0jnhn!UU*A zMLTKV*Y4S6{>+?tC+~gl z^E~&y&zqwgA{Fy2U$q2)d10Z!Zvv3`NC0h)x5k3Iy8*=K!-9i0sdf%Mdj0&Ku+1S| zX=0_NuQdp>MAi9fb?ge8Y{q?;T0S+rjUoT4_z)*L(aop z2^3Qt&OyYAcg~yEaZoFM`Qs3hR7xZASK`3J#(^%Hy%)kkwNvxsE%5a6GCz{Gc4NUj z{Pp`OLK^0q5fsxF%E6xO{%D1e<||Zz3km8+n&y3aE=^nVF)1DjP^&3Q&%GA%tC;ua zfuu`3ScnuqpV>jB5p8Slf532{ZXIxkm@GSzWE+e8L>!yRL(aYUTmO1(h6`6p38G5$ zjl$WTg}m;GB#KFpvhZVW@9<*S_xKbBHMeEJtkSXn3Pth9)fi?EF4X7xv$GW#J-W^% z56Ha~#eFgM=&*o0@BfLE*K?Sw#!fmV)H`!PP?2H=&InVDOGq6Iy7v1a9K4CX zkQwh}Pf$OLM+HiqRBT+0a0&l&a1ka^fj<+>0oAnhcO?D2XQ!1uP2n-~5f4e?mvElI zoI{FT4ixd=d#trV#A#0Okg%-uiWY7i*u9Tp9Fa8ZqPM>uLDZI*qim{9^3X;l)EcF1 z3Y)_NUHI-M6jzjfK0lQgF=<5*ogOd87vK~p~Yg77)a}T>&YvM8Uw_ zr!*@s;K5PW7}|j}$5{V6yf;O`Fp|zDoyL^cw~eQWkgEA%%|W=<83i}YF$BT5-9C#k z%J|>Ixbvye`6Q{wBnZj}b6ya4=wUQTcFK0Xf{OJaNzEPAu#RL~zibk5N|qqx8;+gS zVVT&)Jgt@dh(JjaQdT!R|BHaqDK=_ZNb+;)+gpmeie zfTQLby$`Az zWTa-=hUVWlT{JBxh|%}DpBP+-Sa{OoFtLSX!#|JfL0DBExm(dZ*+Oc#3-%UBT~@qf zV-!42;b8xC&FmTcBuZ@18IY|}mzuLzZ6W%y^@1o4zFX|I4YSH|_-({=vQKA5YTB<3 ztwMy-PS+ah6-cI`cJ1t^kaPdtHWe;^XIvA)#8v-x`D0?v;LN~d<6hHyh~|Uu>PG^x zoa%0R|DvV1q9qMCu*k+N*EwK<aXJb=Df1XUJc1N< z2EHGi;SS~doqS5MC2ZMxfd?7m4VG?iB_pHq&?C_^)V;$N6jzhco`Kv?E>9jw%}0hu zl8?VwOfZ$VtHuz~y6c4kic=>L8`ysw%fkK?v^2NdlOOhH;qq*|@d1K==4fh1Dz+q5 z{#y@Yg}iLXkrSRQblZRC#e!w96;C#8UWg@UT^MJ#irC+Q!X4^(UWUEwjrEVFpGdI; zQy-NZ!y&kT>7D=-o3<*Rz;N5aF7#qIi`*AjS4;}$sQmAzk}(N72X2<7j~_ZbJCQo@ z`OM#I5;J~%F*T4Fe(`S}WJWwHJKsw`AhE5Lf=-t{oz(HeJEo~a^~UN02UDe^+i?8b zy859Vc;B&aTZSiGNIT_&;>`JB%knztEAWPu0qy%J)bHx7MW3PeY+LLtD82ow-Y0`N zh*vGGguYs=zlLwU6$}H?<&JNMao{D|k<3BNS1qb6CxZVXkoI zI>THS{Xf_yI@??GGfeYuZ!}HoN79YjMAJ-)Y4)7)=guyzB;&=Mfq{V z-am61eQ{TXfYM7NY!+2z;h`$1W!YVHw7pJIsw^m0UbA+`-TP?Yi&oX|+RhE%9c-C2k+BcE(g}j+G{(bM|dkU0*=yDXU$wMJ$8|-_*W_ z*7l9<90Xi{%8>pT$k{Hi2YqW&=~(t3XkO^>{RYIYC?_v=Bc2h`s(DalH!Y$*#6bSC zLpKg07y&)PL_)`2Wgk(j|U))qTcBS_g7zUW+e zu;JL!ti)|@1On((7M;NS65I--PMN|6igCaug_1bL@vQ+_p@ZV>Gx0AlCMNyq-JJW* zx#yjHKA&^$JDILbUM+f31OTg3UQlEJAiN}idJI20t9o?+;>S}I@|R0gQ}@34tac|| z(YKf)%;5Y6wT!6*0qd zK%fahovO{?bc8j}#}>&ENzn{z3x|ya;a*4j(NJNU?&i%b4Dv=MJ(2-YkoBApzsqsZ z;>B+_q_f}`e{F*=ADGa1yF+p8`y52ReK<3mKMh&dxQ$SjND%1@<4vjV6iAYLn)g8! z2sPOI_Eo!$6b0LPA5tVB+0l#UXZgcvDB^u6KM}>)MSoq(iOxZF+tEd zF~^}SnItVCyIk*Bj2v7za9BhjFN8GECnf_i?0gazw;(dUor8rxZ-&zg$ryeDM4lGY z@V%@eD1*Tm47}?vlu7V$*{bfDA#90h`(QhrC1v0}*@wBZRd{D(U6(2Jr(g!Q4d31L zBTa*@?U-85S*D77p`T-wEc|vkP0HF%V;oD#g9tIYH`Wr!UB>5=SXe*jXP^jl-3|S% z1X1-x%t;{B7;R-*oPG3*qbF#_whH67%9n<%xo0TaF7UW|%JdH;ChnZ6Q(bq>e=BJ- zn1|b(+ULq|9sLr6`n5!Fx+IGQS?YbCgi!mmwB6A&;W7#t+TsF)id*!}j>usWB?Ip{PrivcTmag=#c-blK@(9H_{ei|rlTY?sBrRhDJ=Qdy3s_4nl^g`* zTuxBHFU`O;a6rAH4-A8tj{NETzX^inJqf3Y0|lc7g8)dZ(e#nINTOx2@KCWYPX#RC zQJ9KJ0TVuAnBM>lZX4x;HOWG*r@6*l13&DkQD9P+;ZKRWkyTm=m(&Evu{wM=EZYTn z2J4WOWP9(0y2)syC{BFSxr1bvML^*iFb~IGLYldWvFUI)*dLBnp-6+H8Ke4xr0^gU zSlo@sE3u}vP@|45j^&x_ZdD<;PRFmfXd=A>K{=*-W&4|FVQ}f%RkM(*{VK)Xi~?}9 z5aav_Pl-Gw@}S5=G>;bjpX9n^Gt{YE3yU4UtM!J3Eel7CZ+(oAtvY@vCt)wFf5;em zUoUfb**Yg0S4hjRGtGC3sl0RCqdJ9NSP>nBUzNdN_`vne4Zg;7zcmmtKKaqvh$Zf|gT1?;Qh|(~aWLaB2 o4CS{@OgS=bjRA5^yTwe{B_qn#^L9?!jen0o%1))CDN$4Y7tnsWwEzGB literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.bar/bar-thickness-min-interval.json b/test/fixtures/controller.bar/bar-thickness-min-interval.json new file mode 100644 index 00000000000..e5861685cd4 --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-min-interval.json @@ -0,0 +1,40 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [1, null, 3, 4, 5] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "time", + "display": false, + "barPercentage": 1, + "categoryPercentage": 1, + "ticks": { + "source": "labels" + } + }], + "yAxes": [{ + "display": false, + "ticks": { + "beginAtZero": true + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-min-interval.png b/test/fixtures/controller.bar/bar-thickness-min-interval.png new file mode 100644 index 0000000000000000000000000000000000000000..ae01a9b945c4ef691bd0a861cdce518f6c23e872 GIT binary patch literal 5180 zcmeHLT}V@57=FKRCpl-fJT8Mu%42Q1ewDh=Na{G1wlp0gSW(ML`nhSNprqzHvmYzS zriLYy*bf*5DNz}Px{At}7}mvtEuAS=gtgR`ZEoxANcKZFK{vsbj~rCW)3z zeO&CWIR*(gX|d zW^_9}bzk71gxr=Z0C&+eq?ob{jIb_h5@E|o9>*CE zad4-1!F~x(N75xK24@h0ZR&~*vvIZzggkV|xo}z1#W1jI^hEb~7qg`# zS6(zQUNr(Ce1l&j-<`<1 zuW&%RFvHX?M5yVoi=tEKGk}|c)FfoHurPifCMx&=wJic%^;AwncKdm-T*8wJjuZ4-Urd95b&9nFGPT zq@It%PWvR*C(HK&GywdxOA78D7HK1;By2o)6&6%Z8= z6%Z8=6%ZBpYXwY|$m8uzYFmGB^qv2G!^*(>5rcV6TOvGn{OmhyXJ$Z;$$vZOx%9>1 zXlpV1`r;~r1BKqLm?=&E&5Mk@*a$gFlziMglaCDwJ~m8b38Xu$|AVE$^(4hz?hio> z?`ux;zGlffjEg9Wo1=iLDp2vZNkx4R8*<3ffI^9MyaCcpY*+y&te{zq>ys(2K3r3X zDjJB1M^>J3vmDaHE~;9|K#dhtYRt@|xCaU-`=vR8&6mRw9;lqKTjRYcKw1st>IwTs zu+ehRYQ=^{g}*0faU}lrp&J<*@&%qaQcIhcB6>xO@E37sPv$~s?}p~r1>W`$e=7*u LtlZ4&>Y}o5GpccS literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.bar/bar-thickness-multiple.json b/test/fixtures/controller.bar/bar-thickness-multiple.json new file mode 100644 index 00000000000..fc39849ae86 --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-multiple.json @@ -0,0 +1,46 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [1, null, 3, 4, 5] + }, { + "backgroundColor": "#36A2EB", + "data": [5, 4, 3, null, 1] + }, { + "backgroundColor": "#FFCE56", + "data": [3, 5, 2, null, 4] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "time", + "display": false, + "barPercentage": 1, + "categoryPercentage": 1, + "ticks": { + "source": "labels" + } + }], + "yAxes": [{ + "display": false, + "ticks": { + "beginAtZero": true + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-multiple.png b/test/fixtures/controller.bar/bar-thickness-multiple.png new file mode 100644 index 0000000000000000000000000000000000000000..d38405292c3b496ae1414813c6bfd120f83ffc8a GIT binary patch literal 5847 zcmeHLYfuwc6u!GIbupj;96(VMQK3}?v10)-B!wuTLZZ{53egGG+N@1)))r}!getwXq`3F2?@NVfYG`+AUd@CT zQ}={tYNsfw>H<$XNz=4>m8%(8tVpv<)S%*L0R+WDJrc&d%1zHk`unv<^-YGrIq{m& z!&1;;T;Me-9J?x!j~p<#nzVKnGS7XCVp!5O3n$_jmjs=bsPZh?R@f3kiSk4-oT{Ut zXw-2Uj#M<9m%2b~{|q}!mO7EYNazx&Vm_L(k;UD7upOcL&s)MGWX>?VD1c@%A?iSo zH|3TVf@1CgED-r<;p0lR6H(Cc@q8z3sUYu61aM1?Ee|F@2A2ggxhg*w&`chjaD_0p zAxuQek75^4$~aPoQ|YVNEMOJQBu~vE{wg+XfG2z6MhCmAo3kXAgtaH*X9(*+rvrCo zPU8xkU^iO9HtjaB&8u*88#8I2#-tq*ruFUfwNucLjKtR>Z_FOBh$) zx@eSEbPZp#kcGW?efLaFVxMcE+8 zs>bu9OyR^K`QA!q`&uKe+;+W0a*F^R;jQENRKmvuP8K-bOKo>_r-Ll949&xFB6#*n z;Np|HCX0d-J+2kgnlA{Tzsx?haj?idh`b7(&azP`WVU4*vgX)Rb?<5Y)_oTJAF+Xrf8$UEEc%#kNT1e#U zFNwl4CM#AZ?cCjsp z$UCqog9evNf!GS+uCOFJ8`w+fz}Y1L>qJrd;y?|4&)Mji1@e>?_-UzGta7h$Zpn_be%ngfX*^CHtWfIs+x zQQ<}eITlMH!h=EU29)mEX-VWXQZsH7APBc$l`RE(mLSI*Er}o)tcF1=bI?qPc^Lk8 zLXiDAL#Zf99~l*m^(m^Vn@SCae6jph{|uhK*J)HV*__JUSKRVyPZ(&8M9+O?3v!Qx zoD)_yZz&6Wfl*UMOEaAk4csp?%H6>G6&6sn1|sL62ksp!JXhRpXB~FNjyW)ces-0_hGzGg|UKf+WosGaN;5NpL0=ss9!+m7tL_b}MbGt5cIOYLh%~teu6Zs7^Y_Pm z*n%W%sJ}kA9hxn@d}!y0)L-)CWG9CVd4D^*2`3ujpz}+!c3-y?*!xv~%e*4{1yPU6 X8|KBe-5M4ZPf}8qETL*q0C;`9?wd!Y{!UnQH*F?uo$uQO^Qi|!V|hsc`MT&kWG9uouR1^Iv*PL#PKejwmF|U(mX5EM zqNdC}uL_KVn!CAr9)e;xL2A9Q(Nc|A6LBwB!!zt>$Eb|AWx82I*IejU(cUjWcg)ZX zK~gl3IbxX7ruNG7uiIM&!h5a6ZM^WH8j`Z$Vabha!xTbClN3H&!&EXFVr-Qw-#WnI?|bDvr* z6w0Ujd8Ra;kK$_@OzIzSKUMNK>GOfa5VSh;uDpYz%s$GSN`+i3l2r&8Upt(xednSn zpcNAGw&;ha_*69wx<4O&EVv9u46dP&t!8HeoR%rW5F}{q*Fa}d8-Zc8>$V^P1xBtu z=3_Vx_EQ-ikoqG>LvaL62Y&;Z7}8jNP3T1_9cWKd@evJ@=XY?=0VEKdrk`Ok9HkSH z97_TJJXfcKH0@@AlGC6rYoQh=v}lGc?(a>4njU-GZEH}^mMuUO8?YJ}+;Hmq#RSPh z)xAuD`IHr36uS;45p)>G>CEt=Ysnlny=|c5)zkJx1$xpZ~YJ=m$ns8ewUxG`xQxtk76HJM$2#97|;}Bd$uHpfUWIn^WekYxzK*DAq@%oK>WBv#N+rVZxe>RYv zwn*{NW&+stgZPIdF0eb%)M2PhJ*!O0QuwXT)`b1HBq=xsE>%hme_YP$lju~^X+xLU zn6I9DR5};dukX=C1V8ybhbwe4e!13OMqgV>_vA$sy6?)7cLWMVGaJqA8(k)o8L_2B ziB`Lc@}92T8R%OUNmqpk%BX*?_f}kkXFP*}TNDDNl3M4+YQ^kkRfn}A@c&m;aN-5J zRFm>bkFlAfTsUSruXIG!7xOzRDjUM5w9R8HAL}HA;xj8Iag^zTa(D{?hK%;C7%U$f h)L};Qz4VmX;hTBc-E)lg0{oc(5@Hgg+Ye@({Rii=3_t(? literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.bar/bar-thickness-offset.json b/test/fixtures/controller.bar/bar-thickness-offset.json new file mode 100644 index 00000000000..b31eb739d2b --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-offset.json @@ -0,0 +1,47 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [1, null, 3, 4, 5] + }, { + "backgroundColor": "#36A2EB", + "data": [5, 4, 3, null, 1] + }, { + "backgroundColor": "#FFCE56", + "data": [3, 5, 2, null, 4] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "time", + "offset": true, + "display": false, + "barPercentage": 1, + "categoryPercentage": 1, + "ticks": { + "source": "labels" + } + }], + "yAxes": [{ + "display": false, + "ticks": { + "beginAtZero": true + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-offset.png b/test/fixtures/controller.bar/bar-thickness-offset.png new file mode 100644 index 0000000000000000000000000000000000000000..8dcecac88a409dff7867684f0987662f1cc4f46e GIT binary patch literal 6577 zcmeI1dr(tX9>>qU1PCCT2rdYLP1YAwQ1A(uLZWCXB4|sisJwiEyU1H4kPw2dB4Y(B zAdN(VrE80{pdvvGk|-h~V0<P+BsAGF;?NZln|8v(uX9xG*@5C3hE3~{-J6a3a+92_J`hGf# z*|}EDqvA<5x~XMNw^ke;J~Cd^TwKr9Pz1{30{S7nKz1scEzii3Ue=t?%ECbKTey2V zVO^tGWyCcSSwusRYy5n!VAJ<5^Yx*jfc4VQe+(u}XPpN{Ml_mXZ0~yDH5d5gJ2bRk zHD4#*+y*AcR?z0bs09#&&6I1d0Po<_!29;@2nPQe1H>#D>avDw)pOulRQfUq?uFq* zW`?UPn(<^6n&H&mfs-^# zL(jg16E|O@2nuca()x>K@BRknpFP}=g zLIj3olwgSJG_x6&47>0+8aqbH+gpVdg}b14b%uQN^gw|Xw!IFZuXvocMv=E9)h_x_ z=$$FnWU$T+2otoo3TNMLHB9y2o^2&gG=UR@Y9X3&sbjc=pJpY&hv>@&m6QlobFA)1 z9vs+Ghcs)RSKC^XM{}03T!8S7Pl6|@4%}ELdK>_56K8D*{H)b3Sg?Xwbe4he1Jm?J z0R8Y^<1YoUZ`eMBUFB}xb_kIS?{-00hCKuHK8_)SyZdc6!w9-a?b`vW7PfS2nFLEF z{Ljl__%FaCfj8s8%ic4Q9L%C>q$S)VIM6gS(=QjK85>I2$^?@IsE~ESq1L5v!)O*)fitVLD+)`m?FUb zm_d#m}9b--exEpVikI*fd)V33#&Lc6MfZI86^41(R3Pl#svzh^VwKbWs7_9V# zuvE9P{}={L(5!sqj0M5^o_?6{XdZyhf%{0r$pf`s1dy*OueK)FeuNMfIFAUI7jaa!}IN}1L}K+@gk zc-FYj9@^?rLswtD$c+t^EQ4Xm#LRIlB8w1AAkSkG%<>3oHj1>nf6f5^`m#8fuOsx|NhUb2S{7!XxV@MZQuUgqy)Yc6Rhkh zaWEVE92ITaD!TsF-MGOcG~+8d+syBaE3U$L2}B=fBKl??iDqo|3oaaVRXn)Vm@#`L zI6E*`YWkXcB)>;#C@(uS_`!;5?@GtR@VJ}3Yfz#E{%$}(dp(qHdMp@z{{)4oKa;*N zUzTHDp%Uv;iK4@Sc~j3z1T8}1A3l6aiz>LmRpe2|mO|L|9a*w~)~2CZ<-t+=;)H*S zwH-B*081*8O!ktB zZDP_Wj?sreB0fs{E3GwxhmvAUa}zBJ%A!#%qWb^LwkTEujvN6PZZ-(xhmWecmG^2T zbmk-a%xro>zKP=}s*H@2s`pSwGp6ns7v{>3muivi5LQ3c7t0 z8N_<>urPrVhFCG34WU!z_-oG;VC9~>jaFI;s=lMN{bXYa+%2S%E)2eSMeDKsmdZxBxZRYclMZED0)lfrdm0Wc` zg@C*O@~6T7q90hf2|AN!$LQ-F|Ce7JZNY-7ps9{GQu2tr42drlgYVK`A0>|JEQ zI{#Opt)DIb#f~7Z;n==uNAfI=(!$#}p-j7=b>wpwrwT6fU$b#qv5qPKJC1aH_G=MK fy92n%3ZcHs(Z24L4=Kp67!bZGYGdPu#H0TJN7H47 literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.bar/bar-thickness-single-xy.json b/test/fixtures/controller.bar/bar-thickness-single-xy.json new file mode 100644 index 00000000000..76caa37fa1a --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-single-xy.json @@ -0,0 +1,40 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [{"x": "2022", "y": 42}] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "time", + "display": false, + "barPercentage": 1, + "categoryPercentage": 1, + "ticks": { + "source": "labels" + } + }], + "yAxes": [{ + "display": false, + "ticks": { + "beginAtZero": true + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-single-xy.png b/test/fixtures/controller.bar/bar-thickness-single-xy.png new file mode 100644 index 0000000000000000000000000000000000000000..26171e531a2b87dd1fcfb8014cb437a531fbe1c4 GIT binary patch literal 4514 zcmeI0`%e^C6vxlK49iGanFX=RW0_r~+7*z{ip63aV0jxvx*%;qC)NfVP$FmwvC6EZ z3L>~z*GD3SL`gw;Y}1w$f^@ID?9<=Qb;#mn#} zAZ3ahH+uH;xrIhzV^=Lxloe`-T! zy#*+GnXSh>PVz{v&^XpsJpbqNsj-oq$+8qy}0O1*@dHpJ+~Iv^*{8~9>) zg5-iJALB|Sw{Iw~lai(FEYq~J?S zwMo}u8p%VpP+ExO56(CO$q^XmSmBRJPh%n$=IiWJtZ)rTRnuurByzlt>yEB~-EK_N z(UOB!HP-3ZR{cIYfaJyI1m+fsp*zjpp=iSVcjZK=a*;ghZ@$fzF=Gb=@L#%-4MUOD zwA=J>6B{r`W1o>0j50@34qxFqkb#y!Z6vDhO193l3icxTo04j)KaxjVVw0^Q)@bHG z#XR}iE84AZ1P79nrZ{5^p0Fd!*h}spxxt*yjAH)ZB~8UpHR;mG(v_tvOIN;i)GNYo^I(l4m zT{D(7UJz`k#Z_D@Zp`}?Dq**vLdbO54PlvV3OW$1mnwAsL-x&&P4=lD-n8j?-{*Os z-~YL!$EaSmBt-CnfDjU*RAlB5LgOn%0w>|)d(-7Lga~_-nXl$mti0>G@Wtw%LwL@} zJvX}rBN->}Ww48Os16UNEAkUgFP9$us{xnMu?+KeSMNK{pm~X)^seCb8fBI?V@;IWm954q8B|@o{@U}lr-?yLG zV()y+3lz$NWF-28+#qJVrV-OPtm*^usPJUCMJ#~{nIFtFvYl8^CIg5oq#cS z@!?b;?tIyi8KR?k*5~h58 z-a>c9Y@%>S5!kfnrT5x=1>p%{bdDQK;PV6v<14DR&LbTGN21`P6t{|eSe_dWpfOV= zOrMFRH_Dp29RkL9X%?<+3zm`FL36M+&&2*L8V~Mv&Ei7UMkDG}L|yN`DCHOb(36M# z35dF8@-Hj1skLEexI8i9#45+W4LQDo7@H(X(+tgKSY|l`&XMq<4PpN0#w3u!J%UgTD|In&k*KqpwzW-YeLK&EFPq8QXteC4Oei>7^-4T zT9T5geJ_QplMY1PPaXd?m$%68|D{R zGFKsvvzi6O@%0V>bxDyB(@}b%+cj@$DA{)lGZE${piC>_)`XZ!1`lSuY(uCW4n$oM zYbEE7TIy`%;wdv%C;bq2gmn$Zk+O^kp@fmV!BC2&?opMmARN8Kc5r~}Fl!-qSzCX^ zBlG~tAjBztjR8b`iUvT-L`aBG^b12<{9y;#b!|GTojC)PmS%2EhzY57T??I&gHTs% z5cRn*HMvpHxip8A{;uNc`fHTRgMB3^!apvanVbG`;U5?Ne;3dG&GL^6|G4=7#)b3Y9D-L8dmEzmI>>Vurr}-0 d)*jl+zg+8GC)Z!M<9!EFE?$=TO-Au2{{nTa>rMaw literal 0 HcmV?d00001 diff --git a/test/fixtures/controller.bar/bar-thickness-stacked.json b/test/fixtures/controller.bar/bar-thickness-stacked.json new file mode 100644 index 00000000000..aa2d825533f --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-stacked.json @@ -0,0 +1,48 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [1, null, 3, 4, 5] + }, { + "backgroundColor": "#36A2EB", + "data": [5, 4, 3, null, 1] + }, { + "backgroundColor": "#FFCE56", + "data": [3, 5, 2, null, 4] + }] + }, + "options": { + "responsive": false, + "legend": false, + "title": false, + "scales": { + "xAxes": [{ + "type": "time", + "stacked": true, + "display": false, + "barPercentage": 1, + "categoryPercentage": 1, + "ticks": { + "source": "labels" + } + }], + "yAxes": [{ + "display": false, + "stacked": true, + "ticks": { + "beginAtZero": true + } + }] + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-stacked.png b/test/fixtures/controller.bar/bar-thickness-stacked.png new file mode 100644 index 0000000000000000000000000000000000000000..696829ee39b400f7bd77dbc1b930784d3cf42542 GIT binary patch literal 5586 zcmeI0dr(tn7QoNFbD=k&B?Jq!IBS4_Aj(5$&~7QYAYcMnL+g%itJ`2@mSvYUUF{6C zJaWTQ9#u3vEViIQZO83)6_qNW0>Lh8k@`s0;v)oHr6`mTKn)O*>`APYcK_W!c02ha zljJ++e&5MC=lh*=QkJ@Q&3v~P+yG#{Sd{Q80Er(7pk48;Q*|i=;JFXQ330#9d1K^O z$7he;i90{Gy=q?QIq&tUM{bD&CKfbiX_;le(_VO;^IpK`Sy9yH{`lX{d*~sd55DA* z-B`NwN}{_qldPcs?(KO`K*cNftSmk?FqnUOh3t~f{LzRdX0UE_*j!t(axgaA$L7gr zSRhFGXBvyl^0>RkY_8FNv$AEpbM&aL*6WFIgB#6;b$rF);#bWUE*Ey+@!qGzrzm<; zC?AeeD;7I8*&W9~O!3CIT~Wa0zx4?V&AhMb9C|UX1Yz{us(l^>xXB)p>`#u|?$9&( zQj}&(b7IEN@vyDS!O%W-V0W*#p|`PnzqEZh&uTDw=5P@47AYREZNB7Jx4G~|438Z}Z| zlhK>Q_^cSR4s|C!ap^9M%DJU*suFG_k@db9y0=6a`ri}B_E4H=9&~9o{R>| z9bv(}n=e`U@w1n7E_ z&;JlAcCY`KFFbW$)9!ix(^B!vAriiuo~ zgxJdheT_^DbP%Gjf{R94ruXx?Pf~Q%s)6y`mvXEiihhWWQI}MFCQ1*DK|JWWpj2`07RpQ zlSrrM{?GEuG9i52D?e-WlG&Of^G{SMCaqH2H6(^E;^o)aZN@X*!!oncyg%&f;j=Ba zUTOY!p=#ki%NpC=lIS zKu7YUdKLGoOqr9_Yx-=L)h*_k#cdPSoI1-LYyy1U*iQuztnF-oLhZmdPv{T>C8!GG z!v|Mtg+8!O43YqXTdJ?4SqK8MvJ3l!6s+q9b^t9YaN(*bCY}dnUH41eX?DD5(lu=f zhZW!RV;Ovv3Lv2RrU!y!V2V4t7sVv-!1ICxVjez^tEo>;aIXH(`oY+vpES#@+P<02 zGv6A^JPKx{)_#k$F6|dKYoPk9`q1H6E=Vn$oRmunn^xMI*V3+du_Drs(?N#z02m5wzn2peY=h;W9A2KbRQR}R)G}$7O=`MKd52qj+~s-ZlTyWS_ho&i+H4%mw-{C| zQW%W6ZFlu+p(xDDadUAK-7Og?EP8eRqb-YOF8cUzn@e-0G?VI^M@dM}|(h{*(OaTm^XQ#oWnR88RySo|AA~lbUo!H7tv)e%czLx`W~z#9~oC903IqLx@H;2%A?h>R2c0A{DbvmRD%oX6T+FB chart.width) { + break; + } + } + } + }); }); diff --git a/test/specs/global.deprecations.tests.js b/test/specs/global.deprecations.tests.js index f1091464d1e..535b9af3307 100644 --- a/test/specs/global.deprecations.tests.js +++ b/test/specs/global.deprecations.tests.js @@ -1,4 +1,13 @@ describe('Deprecations', function() { + describe('Version 2.8.0', function() { + describe('Chart.layoutService', function() { + it('should be defined and an alias of Chart.layouts', function() { + expect(Chart.layoutService).toBeDefined(); + expect(Chart.layoutService).toBe(Chart.layouts); + }); + }); + }); + describe('Version 2.7.0', function() { describe('Chart.Controller.update(duration, lazy)', function() { it('should add an animation with the provided options', function() { @@ -302,8 +311,8 @@ describe('Deprecations', function() { 'afterLayout' ]; - var override = Chart.layoutService.update; - Chart.layoutService.update = function() { + var override = Chart.layouts.update; + Chart.layouts.update = function() { sequence.push('layoutUpdate'); override.apply(this, arguments); }; @@ -372,5 +381,23 @@ describe('Deprecations', function() { expect(Chart.pluginService).toBe(Chart.plugins); }); }); + + describe('Chart.Legend', function() { + it('should be defined and an instance of Chart.Element', function() { + var legend = new Chart.Legend({}); + expect(Chart.Legend).toBeDefined(); + expect(legend).not.toBe(undefined); + expect(legend instanceof Chart.Element).toBeTruthy(); + }); + }); + + describe('Chart.Title', function() { + it('should be defined and an instance of Chart.Element', function() { + var title = new Chart.Title({}); + expect(Chart.Title).toBeDefined(); + expect(title).not.toBe(undefined); + expect(title instanceof Chart.Element).toBeTruthy(); + }); + }); }); }); diff --git a/test/specs/helpers.core.tests.js b/test/specs/helpers.core.tests.js index b13b7cad2af..80b0640b2cc 100644 --- a/test/specs/helpers.core.tests.js +++ b/test/specs/helpers.core.tests.js @@ -369,4 +369,57 @@ describe('Chart.helpers.core', function() { expect(output.o.a).not.toBe(a1); }); }); + + describe('extend', function() { + it('should merge object properties in target and return target', function() { + var target = {a: 'abc', b: 56}; + var object = {b: 0, c: [2, 5, 6]}; + var result = helpers.extend(target, object); + + expect(result).toBe(target); + expect(target).toEqual({a: 'abc', b: 0, c: [2, 5, 6]}); + }); + it('should merge multiple objects properties in target', function() { + var target = {a: 0, b: 1}; + var o0 = {a: 2, c: 3, d: 4}; + var o1 = {a: 5, c: 6}; + var o2 = {a: 7, e: 8}; + + helpers.extend(target, o0, o1, o2); + + expect(target).toEqual({a: 7, b: 1, c: 6, d: 4, e: 8}); + }); + it('should not deeply merge object properties in target', function() { + var target = {a: {b: 0, c: 1}}; + var object = {a: {b: 2, d: 3}}; + + helpers.extend(target, object); + + expect(target).toEqual({a: {b: 2, d: 3}}); + expect(target.a).toBe(object.a); + }); + }); + + describe('inherits', function() { + it('should return a derived class', function() { + var A = function() {}; + A.prototype.p0 = 41; + A.prototype.p1 = function() { + return '42'; + }; + + A.inherits = helpers.inherits; + var B = A.inherits({p0: 43, p2: [44]}); + var C = A.inherits({p3: 45, p4: [46]}); + var b = new B(); + + expect(b instanceof A).toBeTruthy(); + expect(b instanceof B).toBeTruthy(); + expect(b instanceof C).toBeFalsy(); + + expect(b.p0).toBe(43); + expect(b.p1()).toBe('42'); + expect(b.p2).toEqual([44]); + }); + }); }); diff --git a/test/specs/plugin.legend.tests.js b/test/specs/plugin.legend.tests.js index 9e5518b4c05..189a43c0ca1 100644 --- a/test/specs/plugin.legend.tests.js +++ b/test/specs/plugin.legend.tests.js @@ -1,10 +1,5 @@ // Test the rectangle element describe('Legend block tests', function() { - it('Should be constructed', function() { - var legend = new Chart.Legend({}); - expect(legend).not.toBe(undefined); - }); - it('should have the correct default config', function() { expect(Chart.defaults.global.legend).toEqual({ display: true, diff --git a/test/specs/plugin.title.tests.js b/test/specs/plugin.title.tests.js index edeb32a8b47..28786f05402 100644 --- a/test/specs/plugin.title.tests.js +++ b/test/specs/plugin.title.tests.js @@ -1,11 +1,6 @@ // Test the rectangle element describe('Title block tests', function() { - it('Should be constructed', function() { - var title = new Chart.Title({}); - expect(title).not.toBe(undefined); - }); - it('Should have the correct default config', function() { expect(Chart.defaults.global.title).toEqual({ display: false, diff --git a/test/specs/scale.logarithmic.tests.js b/test/specs/scale.logarithmic.tests.js index 9640b1d3d62..3d79e6cfc37 100644 --- a/test/specs/scale.logarithmic.tests.js +++ b/test/specs/scale.logarithmic.tests.js @@ -690,92 +690,435 @@ describe('Logarithmic Scale tests', function() { expect(chart.scales.yScale1.getLabelForIndex(0, 2)).toBe(150); }); - it('should get the correct pixel value for a point', function() { - var chart = window.acquireChart({ - type: 'line', - data: { - datasets: [{ - xAxisID: 'xScale', // for the horizontal scale - yAxisID: 'yScale', - data: [{x: 10, y: 10}, {x: 5, y: 5}, {x: 1, y: 1}, {x: 25, y: 25}, {x: 78, y: 78}] - }], + describe('when', function() { + var data = [ + { + data: [1, 39], + stack: 'stack' }, - options: { - scales: { - xAxes: [{ - id: 'xScale', - type: 'logarithmic' - }], + { + data: [1, 39], + stack: 'stack' + }, + ]; + var dataWithEmptyStacks = [ + { + data: [] + }, + { + data: [] + } + ].concat(data); + var config = [ + { + axis: 'y', + firstTick: 1, // start of the axis (minimum) + describe: 'all stacks are defined' + }, + { + axis: 'y', + data: dataWithEmptyStacks, + firstTick: 1, + describe: 'not all stacks are defined' + }, + { + axis: 'y', + scale: { yAxes: [{ - id: 'yScale', - type: 'logarithmic' + ticks: { + min: 0 + } }] - } + }, + firstTick: 0, + describe: 'all stacks are defined and ticks.min: 0' + }, + { + axis: 'y', + data: dataWithEmptyStacks, + scale: { + yAxes: [{ + ticks: { + min: 0 + } + }] + }, + firstTick: 0, + describe: 'not stacks are defined and ticks.min: 0' + }, + { + axis: 'x', + firstTick: 1, + describe: 'all stacks are defined' + }, + { + axis: 'x', + data: dataWithEmptyStacks, + firstTick: 1, + describe: 'not all stacks are defined' + }, + { + axis: 'x', + scale: { + xAxes: [{ + ticks: { + min: 0 + } + }] + }, + firstTick: 0, + describe: 'all stacks are defined and ticks.min: 0' + }, + { + axis: 'x', + data: dataWithEmptyStacks, + scale: { + xAxes: [{ + ticks: { + min: 0 + } + }] + }, + firstTick: 0, + describe: 'not all stacks are defined and ticks.min: 0' + }, + ]; + config.forEach(function(setup) { + var scaleConfig = {}; + var type, chartStart, chartEnd; + + if (setup.axis === 'x') { + type = 'horizontalBar'; + chartStart = 'left'; + chartEnd = 'right'; + } else { + type = 'bar'; + chartStart = 'bottom'; + chartEnd = 'top'; } - }); + scaleConfig[setup.axis + 'Axes'] = [{ + type: 'logarithmic' + }]; + Chart.helpers.extend(scaleConfig, setup.scale); + var description = 'dataset has stack option and ' + setup.describe + + ' and axis is "' + setup.axis + '";'; + describe(description, function() { + it('should define the correct axis limits', function() { + var chart = window.acquireChart({ + type: type, + data: { + labels: ['category 1', 'category 2'], + datasets: setup.data || data, + }, + options: { + scales: scaleConfig + } + }); - var xScale = chart.scales.xScale; - expect(xScale.getPixelForValue(80, 0, 0)).toBeCloseToPixel(495); // right - paddingRight - expect(xScale.getPixelForValue(1, 0, 0)).toBeCloseToPixel(37 + 6); // left + paddingLeft + lineSpace - expect(xScale.getPixelForValue(10, 0, 0)).toBeCloseToPixel(278 + 6 / 2); // halfway - expect(xScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(37 + 6); // 0 is invalid, put it on the left. + var axisID = setup.axis + '-axis-0'; + var scale = chart.scales[axisID]; + var firstTick = setup.firstTick; + var lastTick = 80; // last tick (should be first available tick after: 2 * 39) + var start = chart.chartArea[chartStart]; + var end = chart.chartArea[chartEnd]; - expect(xScale.getValueForPixel(495)).toBeCloseToPixel(80); - expect(xScale.getValueForPixel(48)).toBeCloseTo(1, 1e-4); - expect(xScale.getValueForPixel(278)).toBeCloseTo(10, 1e-4); + expect(scale.getPixelForValue(firstTick, 0, 0)).toBe(start); + expect(scale.getPixelForValue(lastTick, 0, 0)).toBe(end); - var yScale = chart.scales.yScale; - expect(yScale.getPixelForValue(80, 0, 0)).toBeCloseToPixel(32); // top + paddingTop - expect(yScale.getPixelForValue(1, 0, 0)).toBeCloseToPixel(484); // bottom - paddingBottom - expect(yScale.getPixelForValue(10, 0, 0)).toBeCloseToPixel(246); // halfway - expect(yScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(484); // 0 is invalid. force it on bottom - - expect(yScale.getValueForPixel(32)).toBeCloseTo(80, 1e-4); - expect(yScale.getValueForPixel(484)).toBeCloseTo(1, 1e-4); - expect(yScale.getValueForPixel(246)).toBeCloseTo(10, 1e-4); + expect(scale.getValueForPixel(start)).toBeCloseTo(firstTick, 4); + expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); + + chart.scales[axisID].options.ticks.reverse = true; // Reverse mode + chart.update(); + + // chartArea might have been resized in update + start = chart.chartArea[chartEnd]; + end = chart.chartArea[chartStart]; + + expect(scale.getPixelForValue(firstTick, 0, 0)).toBe(start); + expect(scale.getPixelForValue(lastTick, 0, 0)).toBe(end); + + expect(scale.getValueForPixel(start)).toBeCloseTo(firstTick, 4); + expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); + }); + }); + }); }); - it('should get the correct pixel value for a point when 0 values are present', function() { - var chart = window.acquireChart({ - type: 'bar', - data: { - datasets: [{ - yAxisID: 'yScale', - data: [0.063, 4, 0, 63, 10, 0.5] - }], - labels: [] + describe('when', function() { + var config = [ + { + dataset: [], + firstTick: 1, // value of the first tick + lastTick: 10, // value of the last tick + describe: 'empty dataset, without ticks.min/max' }, - options: { - scales: { - yAxes: [{ - id: 'yScale', - type: 'logarithmic', - ticks: { - reverse: false - } - }] + { + dataset: [], + scale: {stacked: true}, + firstTick: 1, + lastTick: 10, + describe: 'empty dataset, without ticks.min/max, with stacked: true' + }, + { + data: { + datasets: [ + {data: [], stack: 'stack'}, + {data: [], stack: 'stack'}, + ], + }, + type: 'bar', + firstTick: 1, + lastTick: 10, + describe: 'empty dataset with stack option, without ticks.min/max' + }, + { + data: { + datasets: [ + {data: [], stack: 'stack'}, + {data: [], stack: 'stack'}, + ], + }, + type: 'horizontalBar', + firstTick: 1, + lastTick: 10, + describe: 'empty dataset with stack option, without ticks.min/max' + }, + { + dataset: [], + scale: {ticks: {min: 1}}, + firstTick: 1, + lastTick: 10, + describe: 'empty dataset, ticks.min: 1, without ticks.max' + }, + { + dataset: [], + scale: {ticks: {max: 80}}, + firstTick: 1, + lastTick: 80, + describe: 'empty dataset, ticks.max: 80, without ticks.min' + }, + { + dataset: [], + scale: {ticks: {max: 0.8}}, + firstTick: 0.01, + lastTick: 0.8, + describe: 'empty dataset, ticks.max: 0.8, without ticks.min' + }, + { + dataset: [{x: 10, y: 10}, {x: 5, y: 5}, {x: 1, y: 1}, {x: 25, y: 25}, {x: 78, y: 78}], + firstTick: 1, + lastTick: 80, + describe: 'dataset min point {x: 1, y: 1}, max point {x:78, y:78}' + }, + ]; + config.forEach(function(setup) { + var axes = [ + { + id: 'x', // horizontal scale + start: 'left', + end: 'right' + }, + { + id: 'y', // vertical scale + start: 'bottom', + end: 'top' } - } + ]; + axes.forEach(function(axis) { + var expectation = 'min = ' + setup.firstTick + ', max = ' + setup.lastTick; + describe(setup.describe + ' and axis is "' + axis.id + '"; expect: ' + expectation + ';', function() { + beforeEach(function() { + var xScaleConfig = { + type: 'logarithmic', + }; + var yScaleConfig = { + type: 'logarithmic', + }; + var data = setup.data || { + datasets: [{ + data: setup.dataset + }], + }; + Chart.helpers.extend(xScaleConfig, setup.scale); + Chart.helpers.extend(yScaleConfig, setup.scale); + Chart.helpers.extend(data, setup.data || {}); + this.chart = window.acquireChart({ + type: 'line', + data: data, + options: { + scales: { + xAxes: [xScaleConfig], + yAxes: [yScaleConfig] + } + } + }); + }); + + it('should get the correct pixel value for a point', function() { + var chart = this.chart; + var axisID = axis.id + '-axis-0'; + var scale = chart.scales[axisID]; + var firstTick = setup.firstTick; + var lastTick = setup.lastTick; + var start = chart.chartArea[axis.start]; + var end = chart.chartArea[axis.end]; + + expect(scale.getPixelForValue(firstTick, 0, 0)).toBe(start); + expect(scale.getPixelForValue(lastTick, 0, 0)).toBe(end); + expect(scale.getPixelForValue(0, 0, 0)).toBe(start); // 0 is invalid, put it at the start. + + expect(scale.getValueForPixel(start)).toBeCloseTo(firstTick, 4); + expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); + + chart.scales[axisID].options.ticks.reverse = true; // Reverse mode + chart.update(); + + // chartArea might have been resized in update + start = chart.chartArea[axis.end]; + end = chart.chartArea[axis.start]; + + expect(scale.getPixelForValue(firstTick, 0, 0)).toBe(start); + expect(scale.getPixelForValue(lastTick, 0, 0)).toBe(end); + + expect(scale.getValueForPixel(start)).toBeCloseTo(firstTick, 4); + expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); + }); + }); + }); }); + }); - var yScale = chart.scales.yScale; - expect(yScale.getPixelForValue(70, 0, 0)).toBeCloseToPixel(32); // top + paddingTop - expect(yScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(484); // bottom - paddingBottom - expect(yScale.getPixelForValue(0.063, 0, 0)).toBeCloseToPixel(475); // minNotZero 2% from range - expect(yScale.getPixelForValue(0.5, 0, 0)).toBeCloseToPixel(344); - expect(yScale.getPixelForValue(4, 0, 0)).toBeCloseToPixel(213); - expect(yScale.getPixelForValue(10, 0, 0)).toBeCloseToPixel(155); - expect(yScale.getPixelForValue(63, 0, 0)).toBeCloseToPixel(38.5); - - chart.options.scales.yAxes[0].ticks.reverse = true; // Reverse mode - chart.update(); + describe('when', function() { + var config = [ + { + dataset: [], + scale: {ticks: {min: 0}}, + firstTick: 1, // value of the first tick + lastTick: 10, // value of the last tick + describe: 'empty dataset, ticks.min: 0, without ticks.max' + }, + { + dataset: [], + scale: {ticks: {min: 0, max: 80}}, + firstTick: 1, + lastTick: 80, + describe: 'empty dataset, ticks.min: 0, ticks.max: 80' + }, + { + dataset: [], + scale: {ticks: {min: 0, max: 0.8}}, + firstTick: 0.1, + lastTick: 0.8, + describe: 'empty dataset, ticks.min: 0, ticks.max: 0.8' + }, + { + dataset: [{x: 0, y: 0}, {x: 10, y: 10}, {x: 1.2, y: 1.2}, {x: 25, y: 25}, {x: 78, y: 78}], + firstTick: 1, + lastTick: 80, + describe: 'dataset min point {x: 0, y: 0}, max point {x:78, y:78}, minNotZero {x: 1.2, y: 1.2}' + }, + { + dataset: [{x: 0, y: 0}, {x: 10, y: 10}, {x: 6.3, y: 6.3}, {x: 25, y: 25}, {x: 78, y: 78}], + firstTick: 6, + lastTick: 80, + describe: 'dataset min point {x: 0, y: 0}, max point {x:78, y:78}, minNotZero {x: 6.3, y: 6.3}' + }, + { + dataset: [{x: 10, y: 10}, {x: 1.2, y: 1.2}, {x: 25, y: 25}, {x: 78, y: 78}], + scale: {ticks: {min: 0}}, + firstTick: 1, + lastTick: 80, + describe: 'dataset min point {x: 1.2, y: 1.2}, max point {x:78, y:78}, ticks.min: 0' + }, + { + dataset: [{x: 10, y: 10}, {x: 6.3, y: 6.3}, {x: 25, y: 25}, {x: 78, y: 78}], + scale: {ticks: {min: 0}}, + firstTick: 6, + lastTick: 80, + describe: 'dataset min point {x: 6.3, y: 6.3}, max point {x:78, y:78}, ticks.min: 0' + }, + ]; + config.forEach(function(setup) { + var axes = [ + { + id: 'x', // horizontal scale + start: 'left', + end: 'right' + }, + { + id: 'y', // vertical scale + start: 'bottom', + end: 'top' + } + ]; + axes.forEach(function(axis) { + var expectation = 'min = 0, max = ' + setup.lastTick + ', first tick = ' + setup.firstTick; + describe(setup.describe + ' and axis is "' + axis.id + '"; expect: ' + expectation + ';', function() { + beforeEach(function() { + var xScaleConfig = { + type: 'logarithmic', + }; + var yScaleConfig = { + type: 'logarithmic', + }; + var data = setup.data || { + datasets: [{ + data: setup.dataset + }], + }; + Chart.helpers.extend(xScaleConfig, setup.scale); + Chart.helpers.extend(yScaleConfig, setup.scale); + Chart.helpers.extend(data, setup.data || {}); + this.chart = window.acquireChart({ + type: 'line', + data: data, + options: { + scales: { + xAxes: [xScaleConfig], + yAxes: [yScaleConfig] + } + } + }); + }); + + it('should get the correct pixel value for a point', function() { + var chart = this.chart; + var axisID = axis.id + '-axis-0'; + var scale = chart.scales[axisID]; + var firstTick = setup.firstTick; + var lastTick = setup.lastTick; + var fontSize = chart.options.defaultFontSize; + var start = chart.chartArea[axis.start]; + var end = chart.chartArea[axis.end]; + var sign = scale.isHorizontal() ? 1 : -1; + + expect(scale.getPixelForValue(0, 0, 0)).toBe(start); + expect(scale.getPixelForValue(lastTick, 0, 0)).toBe(end); + expect(scale.getPixelForValue(firstTick, 0, 0)).toBe(start + sign * fontSize); - expect(yScale.getPixelForValue(70, 0, 0)).toBeCloseToPixel(484); // bottom - paddingBottom - expect(yScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(32); // top + paddingTop - expect(yScale.getPixelForValue(0.063, 0, 0)).toBeCloseToPixel(41); // minNotZero 2% from range - expect(yScale.getPixelForValue(0.5, 0, 0)).toBeCloseToPixel(172); - expect(yScale.getPixelForValue(4, 0, 0)).toBeCloseToPixel(303); - expect(yScale.getPixelForValue(10, 0, 0)).toBeCloseToPixel(361); - expect(yScale.getPixelForValue(63, 0, 0)).toBeCloseToPixel(477); + expect(scale.getValueForPixel(start)).toBeCloseTo(0, 4); + expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); + expect(scale.getValueForPixel(start + sign * fontSize)).toBeCloseTo(firstTick, 4); + + chart.scales[axisID].options.ticks.reverse = true; // Reverse mode + chart.update(); + + // chartArea might have been resized in update + start = chart.chartArea[axis.end]; + end = chart.chartArea[axis.start]; + + expect(scale.getPixelForValue(0, 0, 0)).toBe(start); + expect(scale.getPixelForValue(lastTick, 0, 0)).toBe(end); + expect(scale.getPixelForValue(firstTick, 0, 0)).toBe(start - sign * fontSize, 4); + + expect(scale.getValueForPixel(start)).toBeCloseTo(0, 4); + expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); + expect(scale.getValueForPixel(start - sign * fontSize)).toBeCloseTo(firstTick, 4); + }); + }); + }); + }); }); + }); diff --git a/test/specs/scale.time.tests.js b/test/specs/scale.time.tests.js index 98dd774b6be..964e25e080a 100755 --- a/test/specs/scale.time.tests.js +++ b/test/specs/scale.time.tests.js @@ -376,23 +376,36 @@ describe('Time scale tests', function() { var config; beforeEach(function() { config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('time')); + config.ticks.source = 'labels'; + config.time.unit = 'day'; }); - it('should use the min option', function() { - config.time.unit = 'day'; + it('should use the min option when less than first label for building ticks', function() { config.time.min = '2014-12-29T04:00:00'; var scale = createScale(mockData, config); - expect(scale.ticks[0]).toEqual('Dec 31'); + expect(scale.ticks[0]).toEqual('Jan 1'); }); - it('should use the max option', function() { - config.time.unit = 'day'; + it('should use the min option when greater than first label for building ticks', function() { + config.time.min = '2015-01-02T04:00:00'; + + var scale = createScale(mockData, config); + expect(scale.ticks[0]).toEqual('Jan 2'); + }); + + it('should use the max option when greater than last label for building ticks', function() { config.time.max = '2015-01-05T06:00:00'; var scale = createScale(mockData, config); + expect(scale.ticks[scale.ticks.length - 1]).toEqual('Jan 3'); + }); - expect(scale.ticks[scale.ticks.length - 1]).toEqual('Jan 5'); + it('should use the max option when less than last label for building ticks', function() { + config.time.max = '2015-01-02T23:00:00'; + + var scale = createScale(mockData, config); + expect(scale.ticks[scale.ticks.length - 1]).toEqual('Jan 2'); }); }); @@ -571,6 +584,115 @@ describe('Time scale tests', function() { expect(xScale.getLabelForIndex(6, 0)).toBe('2015-01-10T12:00'); }); + it('should get the correct label when time is specified as a string', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'xScale0', + data: [{t: '2015-01-01T20:00:00', y: 10}, {t: '2015-01-02T21:00:00', y: 3}] + }], + }, + options: { + scales: { + xAxes: [{ + id: 'xScale0', + type: 'time', + position: 'bottom' + }], + } + } + }); + + var xScale = chart.scales.xScale0; + expect(xScale.getLabelForIndex(0, 0)).toBeTruthy(); + expect(xScale.getLabelForIndex(0, 0)).toBe('2015-01-01T20:00:00'); + }); + + it('should get the correct label for a timestamp with milliseconds', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'xScale0', + data: [ + {t: +new Date('2018-01-08 05:14:23.234'), y: 10}, + {t: +new Date('2018-01-09 06:17:43.426'), y: 3} + ] + }], + }, + options: { + scales: { + xAxes: [{ + id: 'xScale0', + type: 'time', + position: 'bottom' + }], + } + } + }); + + var xScale = chart.scales.xScale0; + var label = xScale.getLabelForIndex(0, 0); + expect(label).toEqual('Jan 8, 2018 5:14:23.234 am'); + }); + + it('should get the correct label for a timestamp with time', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'xScale0', + data: [ + {t: +new Date('2018-01-08 05:14:23'), y: 10}, + {t: +new Date('2018-01-09 06:17:43'), y: 3} + ] + }], + }, + options: { + scales: { + xAxes: [{ + id: 'xScale0', + type: 'time', + position: 'bottom' + }], + } + } + }); + + var xScale = chart.scales.xScale0; + var label = xScale.getLabelForIndex(0, 0); + expect(label).toEqual('Jan 8, 2018 5:14:23 am'); + }); + + it('should get the correct label for a timestamp representing a date', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'xScale0', + data: [ + {t: +new Date('2018-01-08 00:00:00'), y: 10}, + {t: +new Date('2018-01-09 00:00:00'), y: 3} + ] + }], + }, + options: { + scales: { + xAxes: [{ + id: 'xScale0', + type: 'time', + position: 'bottom' + }], + } + } + }); + + var xScale = chart.scales.xScale0; + var label = xScale.getLabelForIndex(0, 0); + expect(label).toEqual('Jan 8, 2018'); + }); + it('should get the correct pixel for only one data in the dataset', function() { var chart = window.acquireChart({ type: 'line', @@ -690,7 +812,7 @@ describe('Time scale tests', function() { expect(getTicksLabels(scale)).toEqual([ '2017', '2019', '2020', '2025', '2042']); }); - it ('should correctly handle empty `data.labels`', function() { + it ('should correctly handle empty `data.labels` using "day" if `time.unit` is undefined`', function() { var chart = this.chart; var scale = chart.scales.x; @@ -701,6 +823,19 @@ describe('Time scale tests', function() { expect(scale.max).toEqual(+moment().endOf('day') + 1); expect(getTicksLabels(scale)).toEqual([]); }); + it ('should correctly handle empty `data.labels` using `time.unit`', function() { + var chart = this.chart; + var scale = chart.scales.x; + var options = chart.options.scales.xAxes[0]; + + options.time.unit = 'year'; + chart.data.labels = []; + chart.update(); + + expect(scale.min).toEqual(+moment().startOf('year')); + expect(scale.max).toEqual(+moment().endOf('year') + 1); + expect(getTicksLabels(scale)).toEqual([]); + }); }); describe('is "data"', function() { @@ -771,7 +906,7 @@ describe('Time scale tests', function() { expect(getTicksLabels(scale)).toEqual([ '2017', '2018', '2019', '2020', '2025', '2042', '2043']); }); - it ('should correctly handle empty `data.labels`', function() { + it ('should correctly handle empty `data.labels` using "day" if `time.unit` is undefined`', function() { var chart = this.chart; var scale = chart.scales.x; @@ -783,6 +918,21 @@ describe('Time scale tests', function() { expect(getTicksLabels(scale)).toEqual([ '2018', '2020', '2043']); }); + it ('should correctly handle empty `data.labels` and hidden datasets using `time.unit`', function() { + var chart = this.chart; + var scale = chart.scales.x; + var options = chart.options.scales.xAxes[0]; + + options.time.unit = 'year'; + chart.data.labels = []; + var meta = chart.getDatasetMeta(1); + meta.hidden = true; + chart.update(); + + expect(scale.min).toEqual(+moment().startOf('year')); + expect(scale.max).toEqual(+moment().endOf('year') + 1); + expect(getTicksLabels(scale)).toEqual([]); + }); }); }); From 17d36d842d12de65c95ab18ddc93d80f8dd199f3 Mon Sep 17 00:00:00 2001 From: touletan Date: Thu, 19 Jul 2018 00:14:35 -0400 Subject: [PATCH 04/11] fix conflict with Rotation functionality (#5319) --- src/helpers/helpers.canvas.js | 5 ++++- test/specs/element.point.tests.js | 28 ++++++++++++++++------------ 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/helpers/helpers.canvas.js b/src/helpers/helpers.canvas.js index 4104ae90855..5bcfb2e6fb7 100644 --- a/src/helpers/helpers.canvas.js +++ b/src/helpers/helpers.canvas.js @@ -49,14 +49,17 @@ var exports = module.exports = { drawPoint: function(ctx, style, radius, x, y, rotation) { // call draw Symbol with converted radius to width and height // and move x, y to the top left corner - if (this.drawSymbol(ctx, style, radius * 2, radius * 2, x - radius, y - radius), rotation) { + + if (this.drawSymbol(ctx, style, radius * 2, radius * 2, x - radius, y - radius, rotation)) { // Only Stroke when return true ctx.stroke(); + ctx.restore(); } }, drawSymbol: function(ctx, style, width, height, x, y, rotation) { rotation = rotation || 0; + if (style && typeof style === 'object') { var type = style.toString(); if (type === '[object HTMLImageElement]' || type === '[object HTMLCanvasElement]') { diff --git a/test/specs/element.point.tests.js b/test/specs/element.point.tests.js index e78cbad73f0..a5efee2359e 100644 --- a/test/specs/element.point.tests.js +++ b/test/specs/element.point.tests.js @@ -137,7 +137,7 @@ describe('Point element tests', function() { args: [] }, { name: 'translate', - args: [10, 15] + args: [tx, ty] }, { name: 'rotate', args: [25 * Math.PI / 180] @@ -146,7 +146,7 @@ describe('Point element tests', function() { args: [] }, { name: 'arc', - args: [0, 0, 2, 0, 2 * Math.PI] + args: [10, 15, 2, 0, 2 * Math.PI] }, { name: 'closePath', args: [], @@ -179,7 +179,7 @@ describe('Point element tests', function() { args: [] }, { name: 'translate', - args: [10, 15] + args: [tx, ty] }, { name: 'rotate', args: [25 * Math.PI / 180] @@ -227,7 +227,7 @@ describe('Point element tests', function() { args: [] }, { name: 'translate', - args: [10, 15] + args: [tx, ty] }, { name: 'rotate', args: [25 * Math.PI / 180] @@ -292,7 +292,7 @@ describe('Point element tests', function() { args: [] }, { name: 'translate', - args: [10, 15] + args: [tx, ty] }, { name: 'rotate', args: [25 * Math.PI / 180] @@ -343,7 +343,7 @@ describe('Point element tests', function() { args: [] }, { name: 'translate', - args: [10, 15] + args: [tx, ty] }, { name: 'rotate', args: [25 * Math.PI / 180] @@ -391,7 +391,7 @@ describe('Point element tests', function() { args: [] }, { name: 'translate', - args: [10, 15] + args: [tx, ty] }, { name: 'rotate', args: [25 * Math.PI / 180] @@ -439,7 +439,7 @@ describe('Point element tests', function() { args: [] }, { name: 'translate', - args: [10, 15] + args: [tx, ty] }, { name: 'rotate', args: [25 * Math.PI / 180] @@ -499,7 +499,7 @@ describe('Point element tests', function() { args: [] }, { name: 'translate', - args: [10, 15] + args: [tx, ty] }, { name: 'rotate', args: [25 * Math.PI / 180] @@ -541,7 +541,7 @@ describe('Point element tests', function() { args: [] }, { name: 'translate', - args: [10, 15] + args: [tx, ty] }, { name: 'rotate', args: [25 * Math.PI / 180] @@ -585,6 +585,8 @@ describe('Point element tests', function() { y: 15, ctx: mockContext }; + var tx = point._view.x - point._view.radius; + var ty = point._view.y - point._view.radius; point.draw(); @@ -602,7 +604,7 @@ describe('Point element tests', function() { args: [] }, { name: 'translate', - args: [10, 15] + args: [tx, ty] }, { name: 'rotate', args: [0] @@ -611,7 +613,7 @@ describe('Point element tests', function() { args: [] }, { name: 'arc', - args: [0, 0, 2, 0, 2 * Math.PI] + args: [10, 15, 2, 0, 2 * Math.PI] }, { name: 'closePath', args: [], @@ -646,6 +648,8 @@ describe('Point element tests', function() { ctx: mockContext, skip: true }; + var tx = point._view.x - point._view.radius; + var ty = point._view.y - point._view.radius; point.draw(); From d9696ba318d375a68698fd9cb8e37fff2ec58266 Mon Sep 17 00:00:00 2001 From: touletan Date: Thu, 19 Jul 2018 00:27:10 -0400 Subject: [PATCH 05/11] fix test file --- test/specs/element.point.tests.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/specs/element.point.tests.js b/test/specs/element.point.tests.js index a5efee2359e..ac963070f42 100644 --- a/test/specs/element.point.tests.js +++ b/test/specs/element.point.tests.js @@ -648,8 +648,6 @@ describe('Point element tests', function() { ctx: mockContext, skip: true }; - var tx = point._view.x - point._view.radius; - var ty = point._view.y - point._view.radius; point.draw(); From 84e66cfa48cf266d928773a5ce8626e4a4d55dab Mon Sep 17 00:00:00 2001 From: touletan Date: Thu, 19 Jul 2018 15:00:50 -0400 Subject: [PATCH 06/11] remove PointRotation. Integration issue with legend . --- src/helpers/helpers.canvas.js | 6 ++--- test/specs/element.point.tests.js | 40 +++++++++++++++---------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/helpers/helpers.canvas.js b/src/helpers/helpers.canvas.js index 5bcfb2e6fb7..56e741dd4fc 100644 --- a/src/helpers/helpers.canvas.js +++ b/src/helpers/helpers.canvas.js @@ -71,9 +71,9 @@ var exports = module.exports = { return false; } - ctx.save(); - ctx.translate(x, y); - ctx.rotate(rotation * Math.PI / 180); + //ctx.save(); + //ctx.translate(x, y); + //ctx.rotate(rotation * Math.PI / 180); ctx.beginPath(); diff --git a/test/specs/element.point.tests.js b/test/specs/element.point.tests.js index ac963070f42..14ff514001f 100644 --- a/test/specs/element.point.tests.js +++ b/test/specs/element.point.tests.js @@ -132,7 +132,7 @@ describe('Point element tests', function() { }, { name: 'setFillStyle', args: ['rgba(0, 255, 0)'] - }, { +/* }, { name: 'save', args: [] }, { @@ -140,7 +140,7 @@ describe('Point element tests', function() { args: [tx, ty] }, { name: 'rotate', - args: [25 * Math.PI / 180] + args: [25 * Math.PI / 180] */ }, { name: 'beginPath', args: [] @@ -174,7 +174,7 @@ describe('Point element tests', function() { }, { name: 'setFillStyle', args: ['rgba(0, 255, 0)'] - }, { +/* }, { name: 'save', args: [] }, { @@ -182,7 +182,7 @@ describe('Point element tests', function() { args: [tx, ty] }, { name: 'rotate', - args: [25 * Math.PI / 180] + args: [25 * Math.PI / 180]*/ }, { name: 'beginPath', args: [] @@ -222,7 +222,7 @@ describe('Point element tests', function() { }, { name: 'setFillStyle', args: ['rgba(0, 255, 0)'] - }, { +/* }, { name: 'save', args: [] }, { @@ -230,7 +230,7 @@ describe('Point element tests', function() { args: [tx, ty] }, { name: 'rotate', - args: [25 * Math.PI / 180] + args: [25 * Math.PI / 180] */ }, { name: 'beginPath', args: [] @@ -287,7 +287,7 @@ describe('Point element tests', function() { }, { name: 'setFillStyle', args: ['rgba(0, 255, 0)'] - }, { +/* }, { name: 'save', args: [] }, { @@ -295,7 +295,7 @@ describe('Point element tests', function() { args: [tx, ty] }, { name: 'rotate', - args: [25 * Math.PI / 180] + args: [25 * Math.PI / 180] */ }, { name: 'beginPath', args: [] @@ -338,7 +338,7 @@ describe('Point element tests', function() { }, { name: 'setFillStyle', args: ['rgba(0, 255, 0)'] - }, { +/* }, { name: 'save', args: [] }, { @@ -346,7 +346,7 @@ describe('Point element tests', function() { args: [tx, ty] }, { name: 'rotate', - args: [25 * Math.PI / 180] + args: [25 * Math.PI / 180] */ }, { name: 'beginPath', args: [] @@ -386,7 +386,7 @@ describe('Point element tests', function() { }, { name: 'setFillStyle', args: ['rgba(0, 255, 0)'] - }, { +/* }, { name: 'save', args: [] }, { @@ -394,7 +394,7 @@ describe('Point element tests', function() { args: [tx, ty] }, { name: 'rotate', - args: [25 * Math.PI / 180] + args: [25 * Math.PI / 180] */ }, { name: 'beginPath', args: [] @@ -434,7 +434,7 @@ describe('Point element tests', function() { }, { name: 'setFillStyle', args: ['rgba(0, 255, 0)'] - }, { +/* }, { name: 'save', args: [] }, { @@ -442,7 +442,7 @@ describe('Point element tests', function() { args: [tx, ty] }, { name: 'rotate', - args: [25 * Math.PI / 180] + args: [25 * Math.PI / 180] */ }, { name: 'beginPath', args: [] @@ -494,7 +494,7 @@ describe('Point element tests', function() { }, { name: 'setFillStyle', args: ['rgba(0, 255, 0)'] - }, { +/* }, { name: 'save', args: [] }, { @@ -502,7 +502,7 @@ describe('Point element tests', function() { args: [tx, ty] }, { name: 'rotate', - args: [25 * Math.PI / 180] + args: [25 * Math.PI / 180] */ }, { name: 'beginPath', args: [] @@ -536,7 +536,7 @@ describe('Point element tests', function() { }, { name: 'setFillStyle', args: ['rgba(0, 255, 0)'] - }, { +/* }, { name: 'save', args: [] }, { @@ -544,7 +544,7 @@ describe('Point element tests', function() { args: [tx, ty] }, { name: 'rotate', - args: [25 * Math.PI / 180] + args: [25 * Math.PI / 180] */ }, { name: 'beginPath', args: [] @@ -599,7 +599,7 @@ describe('Point element tests', function() { }, { name: 'setFillStyle', args: ['rgba(0,0,0,0.1)'] - }, { +/* }, { name: 'save', args: [] }, { @@ -607,7 +607,7 @@ describe('Point element tests', function() { args: [tx, ty] }, { name: 'rotate', - args: [0] + args: [0] */ }, { name: 'beginPath', args: [] From c54bf7062e0e7652e4390a7aef4851bb1b305374 Mon Sep 17 00:00:00 2001 From: touletan Date: Thu, 19 Jul 2018 22:35:44 -0400 Subject: [PATCH 07/11] insert pointRotation functionality back --- src/helpers/helpers.canvas.js | 82 ++++++++++++---------- test/specs/element.point.tests.js | 112 +++++++++++++----------------- 2 files changed, 94 insertions(+), 100 deletions(-) diff --git a/src/helpers/helpers.canvas.js b/src/helpers/helpers.canvas.js index 56e741dd4fc..5c919b82684 100644 --- a/src/helpers/helpers.canvas.js +++ b/src/helpers/helpers.canvas.js @@ -53,12 +53,13 @@ var exports = module.exports = { if (this.drawSymbol(ctx, style, radius * 2, radius * 2, x - radius, y - radius, rotation)) { // Only Stroke when return true ctx.stroke(); - ctx.restore(); + if (rotation) { + ctx.restore(); + } } }, drawSymbol: function(ctx, style, width, height, x, y, rotation) { - rotation = rotation || 0; if (style && typeof style === 'object') { var type = style.toString(); @@ -70,10 +71,17 @@ var exports = module.exports = { if (isNaN(width) || width <= 0) { return false; } - - //ctx.save(); - //ctx.translate(x, y); - //ctx.rotate(rotation * Math.PI / 180); + var vx, vy; + if (rotation) { + ctx.save(); + ctx.translate(x, y); + ctx.rotate(rotation * Math.PI / 180); + vx = 0; + vy = 0; + } else { + vx = x; + vy = y; + } ctx.beginPath(); @@ -82,71 +90,71 @@ var exports = module.exports = { default: // display standard circle if height and width are the same otherwise display a RectRounded if (width === height) { - ctx.arc(x + width / 2, y + width / 2, width / 2, 0, Math.PI * 2); + ctx.arc(vx + width / 2, vy + height / 2, width / 2, 0, Math.PI * 2); } else { - this.roundedRect(ctx, x, y, width, height, width / 2); + this.roundedRect(ctx, vx, vy, width, height, width / 2); } ctx.closePath(); ctx.fill(); break; case 'rect': - ctx.rect(x, y, width, height); + ctx.rect(vx, vy, width, height); ctx.closePath(); ctx.fill(); break; case 'triangle': - ctx.moveTo(x, y + height); - ctx.lineTo(x + width / 2, y); - ctx.lineTo(x + width, y + height); + ctx.moveTo(vx, vy + height); + ctx.lineTo(vx + width / 2, vy); + ctx.lineTo(vx + width, vy + height); ctx.closePath(); ctx.fill(); break; case 'rectRounded': - this.roundedRect(ctx, x, y, width, height, height * Math.SQRT2 / 4); + this.roundedRect(ctx, vx, vy, width, height, height * Math.SQRT2 / 4); ctx.closePath(); ctx.fill(); break; case 'rectRot': - ctx.moveTo(x, y + height / 2); - ctx.lineTo(x + width / 2, y); - ctx.lineTo(x + width, y + height / 2); - ctx.lineTo(x + width / 2, y + height); + ctx.moveTo(vx, vy + height / 2); + ctx.lineTo(vx + width / 2, vy); + ctx.lineTo(vx + width, vy + height / 2); + ctx.lineTo(vx + width / 2, vy + height); ctx.closePath(); ctx.fill(); break; case 'cross': - ctx.moveTo(x + width / 2, y); - ctx.lineTo(x + width / 2, y + height); - ctx.moveTo(x, y + height / 2); - ctx.lineTo(x + width, y + height / 2); + ctx.moveTo(vx + width / 2, vy); + ctx.lineTo(vx + width / 2, vy + height); + ctx.moveTo(vx, vy + height / 2); + ctx.lineTo(vx + width, vy + height / 2); ctx.closePath(); break; case 'crossRot': - ctx.moveTo(x, y); - ctx.lineTo(x + width, y + height); - ctx.moveTo(x, y + height); - ctx.lineTo(x + width, y); + ctx.moveTo(vx, vy); + ctx.lineTo(vx + width, vy + height); + ctx.moveTo(vx, vy + height); + ctx.lineTo(vx + width, vy); ctx.closePath(); break; case 'star': - ctx.moveTo(x + width / 2, y); - ctx.lineTo(x + width / 2, y + height); - ctx.moveTo(x, y + height / 2); - ctx.lineTo(x + width, y + height / 2); - ctx.moveTo(x, y); - ctx.lineTo(x + width, y + height); - ctx.moveTo(x, y + height); - ctx.lineTo(x + width, y); + ctx.moveTo(vx + width / 2, vy); + ctx.lineTo(vx + width / 2, vy + height); + ctx.moveTo(vx, vy + height / 2); + ctx.lineTo(vx + width, vy + height / 2); + ctx.moveTo(vx, vy); + ctx.lineTo(vx + width, vy + height); + ctx.moveTo(vx, vy + height); + ctx.lineTo(vx + width, vy); ctx.closePath(); break; case 'line': - ctx.moveTo(x, y + height / 2); - ctx.lineTo(x + width, y + height / 2); + ctx.moveTo(vx, vy + height / 2); + ctx.lineTo(vx + width, vy + height / 2); ctx.closePath(); break; case 'dash': - ctx.moveTo(x + width / 2, y + height / 2); - ctx.lineTo(x + width, y + height / 2); + ctx.moveTo(vx + width / 2, vy + height / 2); + ctx.lineTo(vx + width, vy + height / 2); ctx.closePath(); break; } diff --git a/test/specs/element.point.tests.js b/test/specs/element.point.tests.js index 14ff514001f..b8c9e148bb9 100644 --- a/test/specs/element.point.tests.js +++ b/test/specs/element.point.tests.js @@ -132,7 +132,7 @@ describe('Point element tests', function() { }, { name: 'setFillStyle', args: ['rgba(0, 255, 0)'] -/* }, { + }, { name: 'save', args: [] }, { @@ -140,13 +140,13 @@ describe('Point element tests', function() { args: [tx, ty] }, { name: 'rotate', - args: [25 * Math.PI / 180] */ + args: [25 * Math.PI / 180] }, { name: 'beginPath', args: [] }, { name: 'arc', - args: [10, 15, 2, 0, 2 * Math.PI] + args: [2, 2, 2, 0, 2 * Math.PI] }, { name: 'closePath', args: [], @@ -174,7 +174,7 @@ describe('Point element tests', function() { }, { name: 'setFillStyle', args: ['rgba(0, 255, 0)'] -/* }, { + }, { name: 'save', args: [] }, { @@ -182,19 +182,19 @@ describe('Point element tests', function() { args: [tx, ty] }, { name: 'rotate', - args: [25 * Math.PI / 180]*/ + args: [25 * Math.PI / 180] }, { name: 'beginPath', args: [] }, { name: 'moveTo', - args: [tx, ty + tw] + args: [0, tw] }, { name: 'lineTo', - args: [tx + tw / 2, ty], + args: [tw / 2, 0], }, { name: 'lineTo', - args: [tx + tw, ty + tw], + args: [tw, tw], }, { name: 'closePath', args: [], @@ -222,7 +222,7 @@ describe('Point element tests', function() { }, { name: 'setFillStyle', args: ['rgba(0, 255, 0)'] -/* }, { + }, { name: 'save', args: [] }, { @@ -230,13 +230,13 @@ describe('Point element tests', function() { args: [tx, ty] }, { name: 'rotate', - args: [25 * Math.PI / 180] */ + args: [25 * Math.PI / 180] }, { name: 'beginPath', args: [] }, { name: 'rect', - args: [tx, ty, tw, tw] + args: [0, 0, tw, tw] }, { name: 'closePath', args: [], @@ -260,8 +260,8 @@ describe('Point element tests', function() { expect(drawRoundedRectangleSpy).toHaveBeenCalledWith( mockContext, - tx, - ty, + 0, + 0, tw, tw, tw * Math.SQRT2 / 4 @@ -287,7 +287,7 @@ describe('Point element tests', function() { }, { name: 'setFillStyle', args: ['rgba(0, 255, 0)'] -/* }, { + }, { name: 'save', args: [] }, { @@ -295,22 +295,22 @@ describe('Point element tests', function() { args: [tx, ty] }, { name: 'rotate', - args: [25 * Math.PI / 180] */ + args: [25 * Math.PI / 180] }, { name: 'beginPath', args: [] }, { name: 'moveTo', - args: [tx, ty + tw / 2] + args: [0, tw / 2] }, { name: 'lineTo', - args: [tx + tw / 2, ty] + args: [tw / 2, 0] }, { name: 'lineTo', - args: [tx + tw, ty + tw / 2], + args: [tw, tw / 2], }, { name: 'lineTo', - args: [tx + tw / 2, ty + tw], + args: [tw / 2, tw], }, { name: 'closePath', args: [] @@ -338,7 +338,7 @@ describe('Point element tests', function() { }, { name: 'setFillStyle', args: ['rgba(0, 255, 0)'] -/* }, { + }, { name: 'save', args: [] }, { @@ -346,22 +346,22 @@ describe('Point element tests', function() { args: [tx, ty] }, { name: 'rotate', - args: [25 * Math.PI / 180] */ + args: [25 * Math.PI / 180] }, { name: 'beginPath', args: [] }, { name: 'moveTo', - args: [tx + tw / 2, ty] + args: [tw / 2, 0] }, { name: 'lineTo', - args: [tx + tw / 2, ty + tw], + args: [tw / 2, tw], }, { name: 'moveTo', - args: [tx, ty + tw / 2], + args: [0, tw / 2], }, { name: 'lineTo', - args: [tx + tw, ty + tw / 2], + args: [tw, tw / 2], }, { name: 'closePath', args: [], @@ -386,7 +386,7 @@ describe('Point element tests', function() { }, { name: 'setFillStyle', args: ['rgba(0, 255, 0)'] -/* }, { + }, { name: 'save', args: [] }, { @@ -394,22 +394,22 @@ describe('Point element tests', function() { args: [tx, ty] }, { name: 'rotate', - args: [25 * Math.PI / 180] */ + args: [25 * Math.PI / 180] }, { name: 'beginPath', args: [] }, { name: 'moveTo', - args: [tx, ty] + args: [0, 0] }, { name: 'lineTo', - args: [tx + tw, ty + tw], + args: [tw, tw], }, { name: 'moveTo', - args: [tx, ty + tw], + args: [0, tw], }, { name: 'lineTo', - args: [tx + tw, ty], + args: [tw, 0], }, { name: 'closePath', args: [], @@ -434,7 +434,7 @@ describe('Point element tests', function() { }, { name: 'setFillStyle', args: ['rgba(0, 255, 0)'] -/* }, { + }, { name: 'save', args: [] }, { @@ -442,34 +442,34 @@ describe('Point element tests', function() { args: [tx, ty] }, { name: 'rotate', - args: [25 * Math.PI / 180] */ + args: [25 * Math.PI / 180] }, { name: 'beginPath', args: [] }, { name: 'moveTo', - args: [tx + tw / 2, ty] + args: [tw / 2, 0] }, { name: 'lineTo', - args: [tx + tw / 2, ty + tw], + args: [tw / 2, tw], }, { name: 'moveTo', - args: [tx, ty + tw / 2], + args: [0, tw / 2], }, { name: 'lineTo', - args: [tx + tw, ty + tw / 2], + args: [tw, tw / 2], }, { name: 'moveTo', - args: [tx, ty] + args: [0, 0] }, { name: 'lineTo', - args: [tx + tw, ty + tw], + args: [tw, tw], }, { name: 'moveTo', - args: [tx, ty + tw], + args: [0, tw], }, { name: 'lineTo', - args: [tx + tw, ty], + args: [tw, 0], }, { name: 'closePath', args: [], @@ -494,7 +494,7 @@ describe('Point element tests', function() { }, { name: 'setFillStyle', args: ['rgba(0, 255, 0)'] -/* }, { + }, { name: 'save', args: [] }, { @@ -502,16 +502,16 @@ describe('Point element tests', function() { args: [tx, ty] }, { name: 'rotate', - args: [25 * Math.PI / 180] */ + args: [25 * Math.PI / 180] }, { name: 'beginPath', args: [] }, { name: 'moveTo', - args: [tx, ty + tw / 2] + args: [0, tw / 2] }, { name: 'lineTo', - args: [tx + tw, ty + tw / 2], + args: [tw, tw / 2], }, { name: 'closePath', args: [], @@ -536,7 +536,7 @@ describe('Point element tests', function() { }, { name: 'setFillStyle', args: ['rgba(0, 255, 0)'] -/* }, { + }, { name: 'save', args: [] }, { @@ -544,16 +544,16 @@ describe('Point element tests', function() { args: [tx, ty] }, { name: 'rotate', - args: [25 * Math.PI / 180] */ + args: [25 * Math.PI / 180] }, { name: 'beginPath', args: [] }, { name: 'moveTo', - args: [tx + tw / 2, ty + tw / 2] + args: [tw / 2, tw / 2] }, { name: 'lineTo', - args: [tx + tw, ty + tw / 2], + args: [tw, tw / 2], }, { name: 'closePath', args: [], @@ -585,8 +585,6 @@ describe('Point element tests', function() { y: 15, ctx: mockContext }; - var tx = point._view.x - point._view.radius; - var ty = point._view.y - point._view.radius; point.draw(); @@ -599,15 +597,6 @@ describe('Point element tests', function() { }, { name: 'setFillStyle', args: ['rgba(0,0,0,0.1)'] -/* }, { - name: 'save', - args: [] - }, { - name: 'translate', - args: [tx, ty] - }, { - name: 'rotate', - args: [0] */ }, { name: 'beginPath', args: [] @@ -623,9 +612,6 @@ describe('Point element tests', function() { }, { name: 'stroke', args: [] - }, { - name: 'restore', - args: [] }]); }); From e8ad16f5d8c6930ccf7db31622270eb1747319d2 Mon Sep 17 00:00:00 2001 From: touletan Date: Fri, 20 Jul 2018 08:09:53 -0400 Subject: [PATCH 08/11] fix rotated symbol center position. --- src/helpers/helpers.canvas.js | 3 ++- test/specs/element.point.tests.js | 45 ++++++++++++++++++++++++------- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/src/helpers/helpers.canvas.js b/src/helpers/helpers.canvas.js index 5c919b82684..a6f5ba03625 100644 --- a/src/helpers/helpers.canvas.js +++ b/src/helpers/helpers.canvas.js @@ -74,8 +74,9 @@ var exports = module.exports = { var vx, vy; if (rotation) { ctx.save(); - ctx.translate(x, y); + 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 { diff --git a/test/specs/element.point.tests.js b/test/specs/element.point.tests.js index b8c9e148bb9..7b4fb17e8c7 100644 --- a/test/specs/element.point.tests.js +++ b/test/specs/element.point.tests.js @@ -137,10 +137,13 @@ describe('Point element tests', function() { args: [] }, { name: 'translate', - args: [tx, ty] + args: [tx + tw / 2, ty + tw / 2] }, { name: 'rotate', args: [25 * Math.PI / 180] + }, { + name: 'translate', + args: [-tw / 2, -tw / 2] }, { name: 'beginPath', args: [] @@ -179,10 +182,13 @@ describe('Point element tests', function() { args: [] }, { name: 'translate', - args: [tx, ty] + args: [tx + tw / 2, ty + tw / 2] }, { name: 'rotate', args: [25 * Math.PI / 180] + }, { + name: 'translate', + args: [-tw / 2, -tw / 2] }, { name: 'beginPath', args: [] @@ -227,10 +233,13 @@ describe('Point element tests', function() { args: [] }, { name: 'translate', - args: [tx, ty] + args: [tx + tw / 2, ty + tw / 2] }, { name: 'rotate', args: [25 * Math.PI / 180] + }, { + name: 'translate', + args: [-tw / 2, -tw / 2] }, { name: 'beginPath', args: [] @@ -292,10 +301,13 @@ describe('Point element tests', function() { args: [] }, { name: 'translate', - args: [tx, ty] + args: [tx + tw / 2, ty + tw / 2] }, { name: 'rotate', args: [25 * Math.PI / 180] + }, { + name: 'translate', + args: [-tw / 2, -tw / 2] }, { name: 'beginPath', args: [] @@ -343,10 +355,13 @@ describe('Point element tests', function() { args: [] }, { name: 'translate', - args: [tx, ty] + args: [tx + tw / 2, ty + tw / 2] }, { name: 'rotate', args: [25 * Math.PI / 180] + }, { + name: 'translate', + args: [-tw / 2, -tw / 2] }, { name: 'beginPath', args: [] @@ -391,10 +406,13 @@ describe('Point element tests', function() { args: [] }, { name: 'translate', - args: [tx, ty] + args: [tx + tw / 2, ty + tw / 2] }, { name: 'rotate', args: [25 * Math.PI / 180] + }, { + name: 'translate', + args: [-tw / 2, -tw / 2] }, { name: 'beginPath', args: [] @@ -439,10 +457,13 @@ describe('Point element tests', function() { args: [] }, { name: 'translate', - args: [tx, ty] + args: [tx + tw / 2, ty + tw / 2] }, { name: 'rotate', args: [25 * Math.PI / 180] + }, { + name: 'translate', + args: [-tw / 2, -tw / 2] }, { name: 'beginPath', args: [] @@ -499,10 +520,13 @@ describe('Point element tests', function() { args: [] }, { name: 'translate', - args: [tx, ty] + args: [tx + tw / 2, ty + tw / 2] }, { name: 'rotate', args: [25 * Math.PI / 180] + }, { + name: 'translate', + args: [-tw / 2, -tw / 2] }, { name: 'beginPath', args: [] @@ -541,10 +565,13 @@ describe('Point element tests', function() { args: [] }, { name: 'translate', - args: [tx, ty] + args: [tx + tw / 2, ty + tw / 2] }, { name: 'rotate', args: [25 * Math.PI / 180] + }, { + name: 'translate', + args: [-tw / 2, -tw / 2] }, { name: 'beginPath', args: [] From 8380e756d7685c24c0de7409c30a15f4ea1f99c6 Mon Sep 17 00:00:00 2001 From: touletan Date: Fri, 10 Aug 2018 07:39:39 -0400 Subject: [PATCH 09/11] rebase --- src/helpers/helpers.canvas.js | 91 +++++++++++++++---------------- test/specs/element.point.tests.js | 53 +++++++++--------- 2 files changed, 71 insertions(+), 73 deletions(-) diff --git a/src/helpers/helpers.canvas.js b/src/helpers/helpers.canvas.js index e21bf92a793..ee12b2458c1 100644 --- a/src/helpers/helpers.canvas.js +++ b/src/helpers/helpers.canvas.js @@ -51,17 +51,10 @@ var exports = module.exports = { drawPoint: function(ctx, style, radius, x, y, rotation) { // call draw Symbol with converted radius to width and height // and move x, y to the top left corner - - if (this.drawSymbol(ctx, style, radius * 2, radius * 2, x - radius, y - radius, rotation)) { - // Only Stroke when return true - ctx.stroke(); - if (rotation) { - ctx.restore(); - } - } + this.drawSymbol(ctx, style, radius * 2, radius * 2, x - radius, y - radius, rotation, true); }, - drawSymbol: function(ctx, style, width, height, x, y, rotation) { + drawSymbol: function(ctx, style, width, height, x, y, rotation, isPoint) { if (style && typeof style === 'object') { var type = style.toString(); @@ -85,6 +78,12 @@ var exports = module.exports = { vx = x; vy = y; } + var radius = width / 2; + var yRadius = height / 2; + 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.beginPath(); @@ -93,71 +92,69 @@ var exports = module.exports = { default: // display standard circle if height and width are the same otherwise display a RectRounded if (width === height) { - ctx.arc(vx + width / 2, vy + height / 2, width / 2, 0, Math.PI * 2); + ctx.arc(vx + radius, vy + radius, radius, 0, Math.PI * 2); } else { - this.roundedRect(ctx, vx, vy, width, height, width / 2); + this.roundedRect(ctx, vx + padLeft, vy + padTop, -padLeft + padRight, -padTop + padBottom, radius * 0.425); } ctx.closePath(); break; case 'rect': - ctx.rect(vx, vy, width, height); + ctx.rect(vx + padLeft, vy + padTop, -padLeft + padRight, -padTop + padBottom); ctx.closePath(); break; case 'triangle': - ctx.moveTo(vx, vy + height); - ctx.lineTo(vx + width / 2, vy); - ctx.lineTo(vx + width, vy + height); + 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(); - ctx.fill(); break; case 'rectRounded': - this.roundedRect(ctx, vx, vy, width, height, height * Math.SQRT2 / 4); + this.roundedRect(ctx, vx + padLeft, vy + padTop, -padLeft + padRight, -padTop + padBottom, radius * 0.425); ctx.closePath(); - ctx.fill(); break; case 'rectRot': - ctx.moveTo(vx, vy + height / 2); - ctx.lineTo(vx + width / 2, vy); - ctx.lineTo(vx + width, vy + height / 2); - ctx.lineTo(vx + width / 2, vy + height); + 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(vx + width / 2, vy); - ctx.lineTo(vx + width / 2, vy + height); - ctx.moveTo(vx, vy + height / 2); - ctx.lineTo(vx + width, vy + height / 2); - ctx.closePath(); + 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': - ctx.moveTo(vx, vy); - ctx.lineTo(vx + width, vy + height); - ctx.moveTo(vx, vy + height); - ctx.lineTo(vx + width, vy); - ctx.closePath(); + 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(vx + width / 2, vy); - ctx.lineTo(vx + width / 2, vy + height); - ctx.moveTo(vx, vy + height / 2); - ctx.lineTo(vx + width, vy + height / 2); - ctx.moveTo(vx, vy); - ctx.lineTo(vx + width, vy + height); - ctx.moveTo(vx, vy + height); - ctx.lineTo(vx + width, vy); - ctx.closePath(); + 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(vx, vy + height / 2); - ctx.lineTo(vx + width, vy + height / 2); - ctx.closePath(); + ctx.moveTo(vx, vy + yRadius); + ctx.lineTo(vx + width, vy + yRadius); break; case 'dash': - ctx.moveTo(vx + width / 2, vy + height / 2); - ctx.lineTo(vx + width, vy + height / 2); - ctx.closePath(); + ctx.moveTo(vx + radius, vy + yRadius); + ctx.lineTo(vx + width, vy + yRadius); break; } + ctx.fill(); + ctx.stroke(); + if (rotation) { + ctx.restore(); + } return true; }, diff --git a/test/specs/element.point.tests.js b/test/specs/element.point.tests.js index 897c8f1e64f..80f9155f6a7 100644 --- a/test/specs/element.point.tests.js +++ b/test/specs/element.point.tests.js @@ -120,6 +120,7 @@ describe('Chart.elements.Point', function() { 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(); @@ -194,13 +195,13 @@ describe('Chart.elements.Point', function() { args: [] }, { name: 'moveTo', - args: [0, tw] + args: [tw / 2, 0] }, { name: 'lineTo', - args: [tw / 2, 0], + args: [tw / 2 - tw * Math.sqrt(3) / 4, tw * 0.75], }, { name: 'lineTo', - args: [tw, tw], + args: [tw / 2 + tw * Math.sqrt(3) / 4, tw * 0.75], }, { name: 'closePath', args: [], @@ -245,7 +246,7 @@ describe('Chart.elements.Point', function() { args: [] }, { name: 'rect', - args: [0, 0, tw, tw] + args: [tw / 2 - zx, tw / 2 - zx, zx * 2, zx * 2] }, { name: 'closePath', args: [], @@ -269,11 +270,11 @@ describe('Chart.elements.Point', function() { expect(drawRoundedRectangleSpy).toHaveBeenCalledWith( mockContext, - 0, - 0, - tw, - tw, - tw * Math.SQRT2 / 4 + tw / 2 - zx, + tw / 2 - zx, + zx * 2, + zx * 2, + tw * 0.2125 ); expect(mockContext.getCalls()).toContain( jasmine.objectContaining({ @@ -313,16 +314,16 @@ describe('Chart.elements.Point', function() { args: [] }, { name: 'moveTo', - args: [0, tw / 2] + args: [tw / 2 - zx, tw / 2] }, { name: 'lineTo', - args: [tw / 2, 0] + args: [tw / 2, tw / 2 - zx] }, { name: 'lineTo', - args: [tw, tw / 2], + args: [tw / 2 + zx, tw / 2], }, { name: 'lineTo', - args: [tw / 2, tw], + args: [tw / 2, tw / 2 + zx], }, { name: 'closePath', args: [] @@ -378,7 +379,7 @@ describe('Chart.elements.Point', function() { name: 'lineTo', args: [tw, tw / 2], }, { - name: 'closePath', + name: 'fill', args: [], }, { name: 'stroke', @@ -418,18 +419,18 @@ describe('Chart.elements.Point', function() { args: [] }, { name: 'moveTo', - args: [0, 0] + args: [tw / 2 - zx, tw / 2 - zx] }, { name: 'lineTo', - args: [tw, tw], + args: [tw / 2 + zx, tw / 2 + zx], }, { name: 'moveTo', - args: [0, tw], + args: [tw / 2 - zx, tw / 2 + zx], }, { name: 'lineTo', - args: [tw, 0], + args: [tw / 2 + zx, tw / 2 - zx], }, { - name: 'closePath', + name: 'fill', args: [], }, { name: 'stroke', @@ -481,18 +482,18 @@ describe('Chart.elements.Point', function() { args: [tw, tw / 2], }, { name: 'moveTo', - args: [0, 0] + args: [tw / 2 - zx, tw / 2 - zx] }, { name: 'lineTo', - args: [tw, tw], + args: [tw / 2 + zx, tw / 2 + zx], }, { name: 'moveTo', - args: [0, tw], + args: [tw / 2 - zx, tw / 2 + zx], }, { name: 'lineTo', - args: [tw, 0], + args: [tw / 2 + zx, tw / 2 - zx], }, { - name: 'closePath', + name: 'fill', args: [], }, { name: 'stroke', @@ -537,7 +538,7 @@ describe('Chart.elements.Point', function() { name: 'lineTo', args: [tw, tw / 2], }, { - name: 'closePath', + name: 'fill', args: [], }, { name: 'stroke', @@ -582,7 +583,7 @@ describe('Chart.elements.Point', function() { name: 'lineTo', args: [tw, tw / 2], }, { - name: 'closePath', + name: 'fill', args: [], }, { name: 'stroke', From 36655918d82a66c1dde8f727231ac9794a237bca Mon Sep 17 00:00:00 2001 From: touletan Date: Tue, 14 Aug 2018 21:30:10 -0400 Subject: [PATCH 10/11] add/fix comments --- docs/configuration/legend.md | 2 +- src/helpers/helpers.canvas.js | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/configuration/legend.md b/docs/configuration/legend.md index ecdb7c824a1..9729ce0001b 100644 --- a/docs/configuration/legend.md +++ b/docs/configuration/legend.md @@ -78,7 +78,7 @@ Items passed to the legend `onClick` function are the ones returned from `labels // Legend symbol to display if point style is not used. symbol: String - // Width of legend symbol if point style is not used. + // Width of legend symbol if point style is not used. boxWidth: Number } ``` diff --git a/src/helpers/helpers.canvas.js b/src/helpers/helpers.canvas.js index ee12b2458c1..b3a50c08cd4 100644 --- a/src/helpers/helpers.canvas.js +++ b/src/helpers/helpers.canvas.js @@ -49,7 +49,7 @@ var exports = module.exports = { }, drawPoint: function(ctx, style, radius, x, y, rotation) { - // call draw Symbol with converted radius to width and height + // 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); }, @@ -80,6 +80,11 @@ var exports = module.exports = { } var radius = width / 2; var yRadius = height / 2; + + // some symbols are not usign 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; From 6e6fe02b6456303cf8ae7772b797b7e7288c7f05 Mon Sep 17 00:00:00 2001 From: touletan Date: Wed, 15 Aug 2018 08:09:38 -0400 Subject: [PATCH 11/11] fix comments --- src/helpers/helpers.canvas.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/helpers.canvas.js b/src/helpers/helpers.canvas.js index b3a50c08cd4..ccc1b53eff4 100644 --- a/src/helpers/helpers.canvas.js +++ b/src/helpers/helpers.canvas.js @@ -81,7 +81,7 @@ var exports = module.exports = { var radius = width / 2; var yRadius = height / 2; - // some symbols are not usign the full width when they're called as a point + // 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.