From 3f2d7efc70409f9a8fd70ea093bf008f592132af Mon Sep 17 00:00:00 2001 From: etimberg Date: Mon, 7 Nov 2016 21:46:31 -0500 Subject: [PATCH 01/61] Add a function to filter items out of the legend --- docs/01-Chart-Configuration.md | 1 + src/core/core.legend.js | 14 ++++++-- test/core.legend.tests.js | 65 ++++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 2 deletions(-) diff --git a/docs/01-Chart-Configuration.md b/docs/01-Chart-Configuration.md index a4692208892..571a2052471 100644 --- a/docs/01-Chart-Configuration.md +++ b/docs/01-Chart-Configuration.md @@ -154,6 +154,7 @@ fontColor | Color | "#666" | Font color inherited from global configuration fontFamily | String | "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif" | Font family inherited from global configuration padding | Number | 10 | Padding between rows of colored boxes generateLabels: | Function | `function(chart) { }` | Generates legend items for each thing in the legend. Default implementation returns the text + styling for the color box. See [Legend Item](#chart-configuration-legend-item-interface) for details. +filter | Function | null | Filters legend items out of the legend. Receives 2 parameters, a [Legend Item](#chart-configuration-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). #### Legend Item Interface diff --git a/src/core/core.legend.js b/src/core/core.legend.js index 9ea5787db9d..87426571b97 100644 --- a/src/core/core.legend.js +++ b/src/core/core.legend.js @@ -162,10 +162,20 @@ module.exports = function(Chart) { beforeBuildLabels: noop, buildLabels: function() { var me = this; - me.legendItems = me.options.labels.generateLabels.call(me, me.chart); + var labelOpts = me.options.labels; + var legendItems = labelOpts.generateLabels.call(me, me.chart); + + if (labelOpts.filter) { + legendItems = legendItems.filter(function(item) { + return labelOpts.filter(item, me.chart.data); + }); + } + if (me.options.reverse) { - me.legendItems.reverse(); + legendItems.reverse(); } + + me.legendItems = legendItems; }, afterBuildLabels: noop, diff --git a/test/core.legend.tests.js b/test/core.legend.tests.js index 6d539fd1ec2..3cdeddc903c 100644 --- a/test/core.legend.tests.js +++ b/test/core.legend.tests.js @@ -90,6 +90,71 @@ describe('Legend block tests', function() { }]); }); + it('should filter items', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + label: 'dataset1', + backgroundColor: '#f31', + borderCapStyle: 'butt', + borderDash: [2, 2], + borderDashOffset: 5.5, + data: [] + }, { + label: 'dataset2', + hidden: true, + borderJoinStyle: 'miter', + data: [], + legendHidden: true + }, { + label: 'dataset3', + borderWidth: 10, + borderColor: 'green', + pointStyle: 'crossRot', + data: [] + }], + labels: [] + }, + options: { + legend: { + labels: { + filter: function(legendItem, data) { + var dataset = data.datasets[legendItem.datasetIndex]; + return !dataset.legendHidden; + } + } + } + } + }); + + expect(chart.legend.legendItems).toEqual([{ + text: 'dataset1', + fillStyle: '#f31', + hidden: false, + lineCap: 'butt', + lineDash: [2, 2], + lineDashOffset: 5.5, + lineJoin: undefined, + lineWidth: undefined, + strokeStyle: undefined, + pointStyle: undefined, + datasetIndex: 0 + }, { + text: 'dataset3', + fillStyle: undefined, + hidden: false, + lineCap: undefined, + lineDash: undefined, + lineDashOffset: undefined, + lineJoin: undefined, + lineWidth: 10, + strokeStyle: 'green', + pointStyle: 'crossRot', + datasetIndex: 2 + }]); + }); + it('should draw correctly', function() { var chart = window.acquireChart({ type: 'bar', From 9ac0293b1ab57d058bd736bc175bed1544dd9331 Mon Sep 17 00:00:00 2001 From: 38elements Date: Sun, 13 Nov 2016 11:15:16 +0900 Subject: [PATCH 02/61] Fix path in 09-Advanced.md --- docs/09-Advanced.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/09-Advanced.md b/docs/09-Advanced.md index 062bf59e8fb..f76ac4b0d97 100644 --- a/docs/09-Advanced.md +++ b/docs/09-Advanced.md @@ -182,7 +182,7 @@ var myPieChart = new Chart(ctx, { }); ``` -See `sample/line-customTooltips.html` for examples on how to get started. +See `samples/tooltips/line-customTooltips.html` for examples on how to get started. ### Writing New Scale Types From 68a2c8240fab9377c8edb2d3923b5f28cc49f771 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Wed, 9 Nov 2016 19:53:20 -0500 Subject: [PATCH 03/61] remove unused cancel animation frame method --- src/core/core.helpers.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index f085290c045..542088f610b 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -671,16 +671,6 @@ module.exports = function(Chart) { return window.setTimeout(callback, 1000 / 60); }; }()); - helpers.cancelAnimFrame = (function() { - return window.cancelAnimationFrame || - window.webkitCancelAnimationFrame || - window.mozCancelAnimationFrame || - window.oCancelAnimationFrame || - window.msCancelAnimationFrame || - function(callback) { - return window.clearTimeout(callback, 1000 / 60); - }; - }()); // -- DOM methods helpers.getRelativePosition = function(evt, chart) { var mouseX, mouseY; From 3985d50201cafe53cbb9abad5910fae2553e83e5 Mon Sep 17 00:00:00 2001 From: etimberg Date: Sun, 13 Nov 2016 19:44:42 -0500 Subject: [PATCH 04/61] use correct option in radar chart --- docs/05-Radar-Chart.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/05-Radar-Chart.md b/docs/05-Radar-Chart.md index f191c3a8178..8fc560b3f04 100644 --- a/docs/05-Radar-Chart.md +++ b/docs/05-Radar-Chart.md @@ -46,7 +46,7 @@ pointBackgroundColor | `Color or Array` | The fill color for points pointBorderWidth | `Number or Array` | The width of the point border in pixels pointRadius | `Number or Array` | The radius of the point shape. If set to 0, nothing is rendered. pointHoverRadius | `Number or Array` | The radius of the point when hovered -hitRadius | `Number or Array` | The pixel size of the non-displayed point that reacts to mouse events +pointHitRadius | `Number or Array` | The pixel size of the non-displayed point that reacts to mouse events pointHoverBackgroundColor | `Color or Array` | Point background color when hovered pointHoverBorderColor | `Color or Array` | Point border color when hovered pointHoverBorderWidth | `Number or Array` | Border width of point when hovered From 339265d21edcb470213055294002edd3db4036d5 Mon Sep 17 00:00:00 2001 From: etimberg Date: Sun, 13 Nov 2016 20:55:54 -0500 Subject: [PATCH 05/61] use correct option for setting tension on radar charts --- src/controllers/controller.radar.js | 2 +- test/controller.radar.tests.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/controller.radar.js b/src/controllers/controller.radar.js index cd62da4768e..fc0c4a285c9 100644 --- a/src/controllers/controller.radar.js +++ b/src/controllers/controller.radar.js @@ -95,7 +95,7 @@ module.exports = function(Chart) { y: reset ? scale.yCenter : pointPosition.y, // Appearance - tension: custom.tension ? custom.tension : helpers.getValueOrDefault(dataset.tension, me.chart.options.elements.line.tension), + tension: custom.tension ? custom.tension : helpers.getValueOrDefault(dataset.lineTension, me.chart.options.elements.line.tension), radius: custom.radius ? custom.radius : helpers.getValueAtIndexOrDefault(dataset.pointRadius, index, pointElementOptions.radius), backgroundColor: custom.backgroundColor ? custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.pointBackgroundColor, index, pointElementOptions.backgroundColor), borderColor: custom.borderColor ? custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.pointBorderColor, index, pointElementOptions.borderColor), diff --git a/test/controller.radar.tests.js b/test/controller.radar.tests.js index 449d295c9c4..b3ee711e16a 100644 --- a/test/controller.radar.tests.js +++ b/test/controller.radar.tests.js @@ -179,7 +179,7 @@ describe('Radar controller tests', function() { }); // Use dataset level styles for lines & points - chart.data.datasets[0].tension = 0; + chart.data.datasets[0].lineTension = 0; chart.data.datasets[0].backgroundColor = 'rgb(98, 98, 98)'; chart.data.datasets[0].borderColor = 'rgb(8, 8, 8)'; chart.data.datasets[0].borderWidth = 0.55; From afab387cc15cd923ca0bc39399da6c7e2d5581c7 Mon Sep 17 00:00:00 2001 From: 38elements Date: Wed, 16 Nov 2016 21:21:44 +0900 Subject: [PATCH 06/61] Fix link in 01-Chart-Configuration.md (#3607) --- docs/01-Chart-Configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/01-Chart-Configuration.md b/docs/01-Chart-Configuration.md index 571a2052471..55f7ddb6eb9 100644 --- a/docs/01-Chart-Configuration.md +++ b/docs/01-Chart-Configuration.md @@ -342,7 +342,7 @@ onComplete | Function | none | Callback called at the end of an animation. Passe #### Animation Callbacks -The `onProgress` and `onComplete` callbacks are useful for synchronizing an external draw to the chart animation. The callback is passed an object that implements the following interface. An example usage of these callbacks can be found on [Github](https://github.com/chartjs/Chart.js/blob/master/samples/AnimationCallbacks/progress-bar.html). This sample displays a progress bar showing how far along the animation is. +The `onProgress` and `onComplete` callbacks are useful for synchronizing an external draw to the chart animation. The callback is passed an object that implements the following interface. An example usage of these callbacks can be found on [Github](https://github.com/chartjs/Chart.js/blob/master/samples/animation/progress-bar.html). This sample displays a progress bar showing how far along the animation is. ```javascript { From 0b4123b92cea1bae5d627349a18b81e5091cad83 Mon Sep 17 00:00:00 2001 From: Jonathon Hill Date: Thu, 17 Nov 2016 04:29:36 -0500 Subject: [PATCH 07/61] Update the documentation plugin list (#3610) Add chartjs-plugin-draggable to the list of plugins, update existing ones recently renamed and re-order the list by names. --- docs/10-Notes.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/10-Notes.md b/docs/10-Notes.md index fd2125e171f..c6ad42910a1 100644 --- a/docs/10-Notes.md +++ b/docs/10-Notes.md @@ -79,12 +79,13 @@ Built in Chart Types There are many plugins that add additional functionality to Chart.js. Some particularly notable ones are listed here. In addition, many plugins can be found on the [Chart.js GitHub organization](https://github.com/chartjs). - - Chart.Zoom.js - Enable zooming and panning on charts - - Chart.Annotation.js - Draw lines and boxes on chart area + - chartjs-plugin-annotation.js - Draw lines and boxes on chart area + - chartjs-plugin-deferred.js - Defer initial chart update until chart scrolls into viewport + - chartjs-plugin-draggable.js - Makes select chart elements draggable with the mouse + - chartjs-plugin-zoom.js - Enable zooming and panning on charts - Chart.BarFunnel.js - Adds a bar funnel chart type - - Chart.Deferred.js - Defer initial chart update until chart scrolls into viewport - - Chart.Smith.js - Adds a smith chart type - Chart.LinearGauge.js - Adds a linear gauge chart type + - Chart.Smith.js - Adds a smith chart type ### Popular Extensions From f7d60c2606816a410ad7b058ce838b09a15729aa Mon Sep 17 00:00:00 2001 From: etimberg Date: Sun, 13 Nov 2016 20:38:50 -0500 Subject: [PATCH 08/61] Allow line chart to use pointBorderWidth of 0 correctly --- src/controllers/controller.line.js | 6 +++--- test/controller.line.tests.js | 19 +++++++++++++++++++ test/controller.radar.tests.js | 17 +++++++++++++++++ 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/controllers/controller.line.js b/src/controllers/controller.line.js index 59d097b1be7..23d4eedc402 100644 --- a/src/controllers/controller.line.js +++ b/src/controllers/controller.line.js @@ -139,11 +139,11 @@ module.exports = function(Chart) { var dataset = this.getDataset(); var custom = point.custom || {}; - if (custom.borderWidth) { + if (!isNaN(custom.borderWidth)) { borderWidth = custom.borderWidth; - } else if (dataset.pointBorderWidth) { + } else if (!isNaN(dataset.pointBorderWidth)) { borderWidth = helpers.getValueAtIndexOrDefault(dataset.pointBorderWidth, index, borderWidth); - } else if (dataset.borderWidth) { + } else if (!isNaN(dataset.borderWidth)) { borderWidth = dataset.borderWidth; } diff --git a/test/controller.line.tests.js b/test/controller.line.tests.js index 3c66b268ec0..f15c693703b 100644 --- a/test/controller.line.tests.js +++ b/test/controller.line.tests.js @@ -753,4 +753,23 @@ describe('Line controller tests', function() { expect(point._model.borderWidth).toBe(5.5); expect(point._model.radius).toBe(4.4); }); + + it('should allow 0 as a point border width', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [10, 15, 0, -4], + label: 'dataset1', + pointBorderWidth: 0 + }], + labels: ['label1', 'label2', 'label3', 'label4'] + } + }); + + var meta = chart.getDatasetMeta(0); + var point = meta.data[0]; + + expect(point._model.borderWidth).toBe(0); + }); }); diff --git a/test/controller.radar.tests.js b/test/controller.radar.tests.js index b3ee711e16a..4b9c1f696dc 100644 --- a/test/controller.radar.tests.js +++ b/test/controller.radar.tests.js @@ -452,4 +452,21 @@ describe('Radar controller tests', function() { expect(point._model.borderWidth).toBe(5.5); expect(point._model.radius).toBe(4.4); }); + + it('should allow pointBorderWidth to be set to 0', function() { + var chart = window.acquireChart({ + type: 'radar', + data: { + datasets: [{ + data: [10, 15, 0, 4], + pointBorderWidth: 0 + }], + labels: ['label1', 'label2', 'label3', 'label4'] + } + }); + + var meta = chart.getDatasetMeta(0); + var point = meta.data[0]; + expect(point._model.borderWidth).toBe(0); + }); }); From fd2e40ce119225078f7321f979cf099838c99c42 Mon Sep 17 00:00:00 2001 From: Jeff Carey Date: Mon, 7 Nov 2016 20:49:14 -0800 Subject: [PATCH 09/61] Fixed vertical alignment in legend text (#3387) --- src/core/core.legend.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/core.legend.js b/src/core/core.legend.js index 87426571b97..07349fd80b9 100644 --- a/src/core/core.legend.js +++ b/src/core/core.legend.js @@ -414,7 +414,7 @@ module.exports = function(Chart) { } } else if (y + itemHeight > me.bottom) { x = cursor.x = x + me.columnWidths[cursor.line] + labelOpts.padding; - y = cursor.y = me.top; + y = cursor.y = me.top + labelOpts.padding; cursor.line++; } From eaf109c2b1d0ceb8499bc22659197555b88b6df3 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Mon, 24 Oct 2016 21:43:52 -0400 Subject: [PATCH 10/61] When an axis needs padding due to a long, rotated, label it should be added inside the layout system rather than in each axis. --- src/core/core.layoutService.js | 28 +++++++++++++++++++++++----- src/core/core.scale.js | 11 ++++++++++- test/scale.linear.tests.js | 8 ++++---- test/scale.logarithmic.tests.js | 8 ++++---- 4 files changed, 41 insertions(+), 14 deletions(-) diff --git a/src/core/core.layoutService.js b/src/core/core.layoutService.js index 220a2534484..2afdedfa395 100644 --- a/src/core/core.layoutService.js +++ b/src/core/core.layoutService.js @@ -148,15 +148,26 @@ module.exports = function(Chart) { minBoxSizes.push({ horizontal: isHorizontal, minSize: minSize, - box: box + box: box, }); } helpers.each(leftBoxes.concat(rightBoxes, topBoxes, bottomBoxes), getMinimumBoxSize); + // If a box has padding, we move the left scale over to avoid ugly charts (see issue #2478) + var maxHorizontalLeftPadding = 0; + var maxHorizontalRightPadding = 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); + } + }); + // 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; @@ -172,8 +183,8 @@ module.exports = function(Chart) { if (minBoxSize) { if (box.isHorizontal()) { var scaleMargin = { - left: totalLeftBoxesWidth, - right: totalRightBoxesWidth, + left: Math.max(totalLeftBoxesWidth, maxHorizontalLeftPadding), + right: Math.max(totalRightBoxesWidth, maxHorizontalRightPadding), top: 0, bottom: 0 }; @@ -251,6 +262,13 @@ module.exports = function(Chart) { totalBottomBoxesHeight += box.height; }); + // We may be adding some padding to account for rotated x axis labels + var leftPaddingAddition = Math.max(maxHorizontalLeftPadding - totalLeftBoxesWidth, 0); + var rightPaddingAddition = Math.max(maxHorizontalRightPadding - totalRightBoxesWidth, 0); + + totalLeftBoxesWidth += leftPaddingAddition; + totalRightBoxesWidth += rightPaddingAddition; + // 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 @@ -283,7 +301,7 @@ module.exports = function(Chart) { } // Step 7 - Position the boxes - var left = leftPadding; + var left = leftPadding + leftPaddingAddition; var top = topPadding; function placeBox(box) { diff --git a/src/core/core.scale.js b/src/core/core.scale.js index 5f35b999f05..1f6363c0790 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -51,8 +51,17 @@ module.exports = function(Chart) { }; Chart.Scale = Chart.Element.extend({ + getPadding: function() { + var me = this; + return { + left: me.paddingLeft || 0, + top: me.paddingTop || 0, + right: me.paddingRight || 0, + bottom: me.paddingBottom || 0 + }; + }, - // These methods are ordered by lifecycle. Utilities then follow. + // These methods are ordered by lifecyle. Utilities then follow. // Any function defined here is inherited by all scale types. // Any function can be extended by the scale type diff --git a/test/scale.linear.tests.js b/test/scale.linear.tests.js index 7102a6f4a8c..1e18fb529fb 100644 --- a/test/scale.linear.tests.js +++ b/test/scale.linear.tests.js @@ -718,8 +718,8 @@ describe('Linear Scale', function() { expect(xScale.paddingTop).toBeCloseToPixel(0); expect(xScale.paddingBottom).toBeCloseToPixel(0); expect(xScale.paddingLeft).toBeCloseToPixel(0); - expect(xScale.paddingRight).toBeCloseToPixel(13.5); - expect(xScale.width).toBeCloseToPixel(471); + expect(xScale.paddingRight).toBeCloseToPixel(0); + expect(xScale.width).toBeCloseToPixel(457.5); expect(xScale.height).toBeCloseToPixel(28); var yScale = chart.scales.yScale0; @@ -738,8 +738,8 @@ describe('Linear Scale', function() { expect(xScale.paddingTop).toBeCloseToPixel(0); expect(xScale.paddingBottom).toBeCloseToPixel(0); expect(xScale.paddingLeft).toBeCloseToPixel(0); - expect(xScale.paddingRight).toBeCloseToPixel(13.5); - expect(xScale.width).toBeCloseToPixel(453); + expect(xScale.paddingRight).toBeCloseToPixel(0); + expect(xScale.width).toBeCloseToPixel(439.5); expect(xScale.height).toBeCloseToPixel(46); expect(yScale.paddingTop).toBeCloseToPixel(0); diff --git a/test/scale.logarithmic.tests.js b/test/scale.logarithmic.tests.js index 9636a0b375c..4ca9ab62ec4 100644 --- a/test/scale.logarithmic.tests.js +++ b/test/scale.logarithmic.tests.js @@ -717,14 +717,14 @@ describe('Logarithmic Scale tests', function() { }); var xScale = chart.scales.xScale; - expect(xScale.getPixelForValue(80, 0, 0)).toBeCloseToPixel(495); // right - paddingRight + expect(xScale.getPixelForValue(80, 0, 0)).toBeCloseToPixel(481.5); // right - paddingRight expect(xScale.getPixelForValue(1, 0, 0)).toBeCloseToPixel(48); // left + paddingLeft - expect(xScale.getPixelForValue(10, 0, 0)).toBeCloseToPixel(283); // halfway + expect(xScale.getPixelForValue(10, 0, 0)).toBeCloseToPixel(276); // halfway expect(xScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(48); // 0 is invalid, put it on the left. - expect(xScale.getValueForPixel(495)).toBeCloseTo(80, 1e-4); + expect(xScale.getValueForPixel(481.5)).toBeCloseTo(80, 1e-4); expect(xScale.getValueForPixel(48)).toBeCloseTo(1, 1e-4); - expect(xScale.getValueForPixel(283)).toBeCloseTo(10, 1e-4); + expect(xScale.getValueForPixel(276)).toBeCloseTo(10, 1e-4); var yScale = chart.scales.yScale; expect(yScale.getPixelForValue(80, 0, 0)).toBeCloseToPixel(32); // top + paddingTop From 5dd1c77cf51adda1055c2eabb9f064b8ed5ee5ff Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Mon, 24 Oct 2016 22:36:50 -0400 Subject: [PATCH 11/61] Take vertical padding into account --- src/core/core.layoutService.js | 22 +++++++++++++++++----- src/core/core.scale.js | 6 ++++++ test/scale.logarithmic.tests.js | 2 +- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/core/core.layoutService.js b/src/core/core.layoutService.js index 2afdedfa395..39cf26b9006 100644 --- a/src/core/core.layoutService.js +++ b/src/core/core.layoutService.js @@ -154,9 +154,11 @@ module.exports = function(Chart) { helpers.each(leftBoxes.concat(rightBoxes, topBoxes, bottomBoxes), getMinimumBoxSize); - // If a box has padding, we move the left scale over to avoid ugly charts (see issue #2478) + // 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) { @@ -166,6 +168,14 @@ module.exports = function(Chart) { } }); + 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 @@ -264,10 +274,12 @@ module.exports = function(Chart) { // We may be adding some padding to account for rotated x axis labels var leftPaddingAddition = Math.max(maxHorizontalLeftPadding - totalLeftBoxesWidth, 0); - var rightPaddingAddition = Math.max(maxHorizontalRightPadding - totalRightBoxesWidth, 0); - totalLeftBoxesWidth += leftPaddingAddition; - totalRightBoxesWidth += rightPaddingAddition; + 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 @@ -302,7 +314,7 @@ module.exports = function(Chart) { // Step 7 - Position the boxes var left = leftPadding + leftPaddingAddition; - var top = topPadding; + var top = topPadding + topPaddingAddition; function placeBox(box) { if (box.isHorizontal()) { diff --git a/src/core/core.scale.js b/src/core/core.scale.js index 1f6363c0790..32ece041f5b 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -51,6 +51,12 @@ module.exports = function(Chart) { }; Chart.Scale = Chart.Element.extend({ + /** + * Get the padding needed for the scale + * @method getPadding + * @private + * @returns {Padding} the necessary padding + */ getPadding: function() { var me = this; return { diff --git a/test/scale.logarithmic.tests.js b/test/scale.logarithmic.tests.js index 4ca9ab62ec4..14a382e5f76 100644 --- a/test/scale.logarithmic.tests.js +++ b/test/scale.logarithmic.tests.js @@ -722,7 +722,7 @@ describe('Logarithmic Scale tests', function() { expect(xScale.getPixelForValue(10, 0, 0)).toBeCloseToPixel(276); // halfway expect(xScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(48); // 0 is invalid, put it on the left. - expect(xScale.getValueForPixel(481.5)).toBeCloseTo(80, 1e-4); + expect(xScale.getValueForPixel(481.5)).toBeCloseToPixel(80); expect(xScale.getValueForPixel(48)).toBeCloseTo(1, 1e-4); expect(xScale.getValueForPixel(276)).toBeCloseTo(10, 1e-4); From 48cb8b78e73a8a305aa7a19957ce6dcddfd9a6cd Mon Sep 17 00:00:00 2001 From: etimberg Date: Sun, 30 Oct 2016 13:14:59 -0400 Subject: [PATCH 12/61] Remove unnecessary padding usages and update category scale tests --- src/scales/scale.category.js | 14 +- src/scales/scale.linear.js | 18 +- src/scales/scale.logarithmic.js | 34 ++- src/scales/scale.time.js | 13 +- test/scale.category.tests.js | 433 +++++++++++++------------------- 5 files changed, 208 insertions(+), 304 deletions(-) diff --git a/src/scales/scale.category.js b/src/scales/scale.category.js index fb90e6333f1..8a65cb2e7a0 100644 --- a/src/scales/scale.category.js +++ b/src/scales/scale.category.js @@ -73,9 +73,8 @@ module.exports = function(Chart) { } if (me.isHorizontal()) { - var innerWidth = me.width - (me.paddingLeft + me.paddingRight); - var valueWidth = innerWidth / offsetAmt; - var widthOffset = (valueWidth * (index - me.minIndex)) + me.paddingLeft; + var valueWidth = me.width / offsetAmt; + var widthOffset = (valueWidth * (index - me.minIndex)); if (me.options.gridLines.offsetGridLines && includeOffset || me.maxIndex === me.minIndex && includeOffset) { widthOffset += (valueWidth / 2); @@ -83,9 +82,8 @@ module.exports = function(Chart) { return me.left + Math.round(widthOffset); } - var innerHeight = me.height - (me.paddingTop + me.paddingBottom); - var valueHeight = innerHeight / offsetAmt; - var heightOffset = (valueHeight * (index - me.minIndex)) + me.paddingTop; + var valueHeight = me.height / offsetAmt; + var heightOffset = (valueHeight * (index - me.minIndex)); if (me.options.gridLines.offsetGridLines && includeOffset) { heightOffset += (valueHeight / 2); @@ -101,15 +99,13 @@ module.exports = function(Chart) { var value; var offsetAmt = Math.max((me.ticks.length - ((me.options.gridLines.offsetGridLines) ? 0 : 1)), 1); var horz = me.isHorizontal(); - var innerDimension = horz ? me.width - (me.paddingLeft + me.paddingRight) : me.height - (me.paddingTop + me.paddingBottom); - var valueDimension = innerDimension / offsetAmt; + var valueDimension = (horz ? me.width : me.height) / offsetAmt; pixel -= horz ? me.left : me.top; if (me.options.gridLines.offsetGridLines) { pixel -= (valueDimension / 2); } - pixel -= horz ? me.paddingLeft : me.paddingTop; if (pixel <= 0) { value = 0; diff --git a/src/scales/scale.linear.js b/src/scales/scale.linear.js index e8afa84f8f1..301af6bc986 100644 --- a/src/scales/scale.linear.js +++ b/src/scales/scale.linear.js @@ -132,31 +132,25 @@ module.exports = function(Chart) { // This must be called after fit has been run so that // this.left, this.top, this.right, and this.bottom have been defined var me = this; - var paddingLeft = me.paddingLeft; - var paddingBottom = me.paddingBottom; var start = me.start; var rightValue = +me.getRightValue(value); var pixel; - var innerDimension; var range = me.end - start; if (me.isHorizontal()) { - innerDimension = me.width - (paddingLeft + me.paddingRight); - pixel = me.left + (innerDimension / range * (rightValue - start)); - return Math.round(pixel + paddingLeft); + pixel = me.left + (me.width / range * (rightValue - start)); + return Math.round(pixel); } - innerDimension = me.height - (me.paddingTop + paddingBottom); - pixel = (me.bottom - paddingBottom) - (innerDimension / range * (rightValue - start)); + + pixel = me.bottom - (me.height / range * (rightValue - start)); return Math.round(pixel); }, getValueForPixel: function(pixel) { var me = this; var isHorizontal = me.isHorizontal(); - var paddingLeft = me.paddingLeft; - var paddingBottom = me.paddingBottom; - var innerDimension = isHorizontal ? me.width - (paddingLeft + me.paddingRight) : me.height - (me.paddingTop + paddingBottom); - var offset = (isHorizontal ? pixel - me.left - paddingLeft : me.bottom - paddingBottom - pixel) / innerDimension; + var innerDimension = isHorizontal ? me.width : me.height; + var offset = (isHorizontal ? pixel - me.left : me.bottom - pixel) / innerDimension; return me.start + ((me.end - me.start) * offset); }, getPixelForTick: function(index) { diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js index 9d87459406a..160d35f23fd 100644 --- a/src/scales/scale.logarithmic.js +++ b/src/scales/scale.logarithmic.js @@ -162,46 +162,42 @@ module.exports = function(Chart) { var start = me.start; var newVal = +me.getRightValue(value); var range; - var paddingTop = me.paddingTop; - var paddingBottom = me.paddingBottom; - var paddingLeft = me.paddingLeft; var opts = me.options; var tickOpts = opts.ticks; if (me.isHorizontal()) { range = helpers.log10(me.end) - helpers.log10(start); // todo: if start === 0 if (newVal === 0) { - pixel = me.left + paddingLeft; + pixel = me.left; } else { - innerDimension = me.width - (paddingLeft + me.paddingRight); + innerDimension = me.width; pixel = me.left + (innerDimension / range * (helpers.log10(newVal) - helpers.log10(start))); - pixel += paddingLeft; } } else { // Bottom - top since pixels increase downward on a screen - innerDimension = me.height - (paddingTop + paddingBottom); + innerDimension = me.height; if (start === 0 && !tickOpts.reverse) { range = helpers.log10(me.end) - helpers.log10(me.minNotZero); if (newVal === start) { - pixel = me.bottom - paddingBottom; + pixel = me.bottom; } else if (newVal === me.minNotZero) { - pixel = me.bottom - paddingBottom - innerDimension * 0.02; + pixel = me.bottom - innerDimension * 0.02; } else { - pixel = me.bottom - paddingBottom - innerDimension * 0.02 - (innerDimension * 0.98/ range * (helpers.log10(newVal)-helpers.log10(me.minNotZero))); + 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 + paddingTop; + pixel = me.top; } else if (newVal === me.minNotZero) { - pixel = me.top + paddingTop + innerDimension * 0.02; + pixel = me.top + innerDimension * 0.02; } else { - pixel = me.top + paddingTop + innerDimension * 0.02 + (innerDimension * 0.98/ range * (helpers.log10(newVal)-helpers.log10(me.minNotZero))); + pixel = me.top + innerDimension * 0.02 + (innerDimension * 0.98/ range * (helpers.log10(newVal)-helpers.log10(me.minNotZero))); } } else { range = helpers.log10(me.end) - helpers.log10(start); - innerDimension = me.height - (paddingTop + paddingBottom); - pixel = (me.bottom - paddingBottom) - (innerDimension / range * (helpers.log10(newVal) - helpers.log10(start))); + innerDimension = me.height; + pixel = me.bottom - (innerDimension / range * (helpers.log10(newVal) - helpers.log10(start))); } } return pixel; @@ -212,11 +208,11 @@ module.exports = function(Chart) { var value, innerDimension; if (me.isHorizontal()) { - innerDimension = me.width - (me.paddingLeft + me.paddingRight); - value = me.start * Math.pow(10, (pixel - me.left - me.paddingLeft) * range / innerDimension); + innerDimension = me.width; + value = me.start * Math.pow(10, (pixel - me.left) * range / innerDimension); } else { // todo: if start === 0 - innerDimension = me.height - (me.paddingTop + me.paddingBottom); - value = Math.pow(10, (me.bottom - me.paddingBottom - pixel) * range / innerDimension) / me.start; + innerDimension = me.height; + value = Math.pow(10, (me.bottom - pixel) * range / innerDimension) / me.start; } return value; } diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index c69bcf21e75..0057dd92aea 100755 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -240,7 +240,7 @@ module.exports = function(Chart) { me.unitScale = helpers.getValueOrDefault(me.options.time.unitStepSize, 1); } else { // Determine the smallest needed unit of the time - var innerWidth = me.isHorizontal() ? me.width - (me.paddingLeft + me.paddingRight) : me.height - (me.paddingTop + me.paddingBottom); + var innerWidth = me.isHorizontal() ? me.width : me.height; // Crude approximation of what the label length might be var tempFirstLabel = me.tickFormatFunction(me.firstTick, 0, []); @@ -409,14 +409,11 @@ module.exports = function(Chart) { var decimal = offset !== 0 ? offset / me.scaleSizeInUnits : offset; if (me.isHorizontal()) { - var innerWidth = me.width - (me.paddingLeft + me.paddingRight); - var valueOffset = (innerWidth * decimal) + me.paddingLeft; - + var valueOffset = (me.width * decimal); return me.left + Math.round(valueOffset); } - var innerHeight = me.height - (me.paddingTop + me.paddingBottom); - var heightOffset = (innerHeight * decimal) + me.paddingTop; + var heightOffset = (me.height * decimal); return me.top + Math.round(heightOffset); } }, @@ -425,8 +422,8 @@ module.exports = function(Chart) { }, getValueForPixel: function(pixel) { var me = this; - var innerDimension = me.isHorizontal() ? me.width - (me.paddingLeft + me.paddingRight) : me.height - (me.paddingTop + me.paddingBottom); - var offset = (pixel - (me.isHorizontal() ? me.left + me.paddingLeft : me.top + me.paddingTop)) / innerDimension; + var innerDimension = me.isHorizontal() ? me.width : me.height; + var offset = (pixel - (me.isHorizontal() ? me.left : me.top)) / innerDimension; offset *= me.scaleSizeInUnits; return me.firstTick.clone().add(moment.duration(offset, me.tickUnit).asSeconds(), 'seconds'); }, diff --git a/test/scale.category.tests.js b/test/scale.category.tests.js index 13955d35be4..08cae8ba0b4 100644 --- a/test/scale.category.tests.js +++ b/test/scale.category.tests.js @@ -161,305 +161,226 @@ describe('Category scale tests', function() { }); it ('Should get the correct pixel for a value when horizontal', function() { - var scaleID = 'myScale'; - - var mockData = { - datasets: [{ - yAxisID: scaleID, - data: [10, 5, 0, 25, 78] - }], - labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick_last'] - }; - - var mockContext = window.createMockContext(); - var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('category')); - config.gridLines.offsetGridLines = true; - var Constructor = Chart.scaleService.getScaleConstructor('category'); - var scale = new Constructor({ - ctx: mockContext, - options: config, - chart: { - data: mockData + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'xScale0', + yAxisID: 'yScale0', + data: [10, 5, 0, 25, 78] + }], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick_last'] }, - id: scaleID - }); - - var minSize = scale.update(600, 100); - - expect(scale.width).toBe(600); - expect(scale.height).toBe(28); - expect(scale.paddingTop).toBe(0); - expect(scale.paddingBottom).toBe(0); - expect(scale.paddingLeft).toBe(28); - expect(scale.paddingRight).toBe(48); - expect(scale.labelRotation).toBe(0); - - expect(minSize).toEqual({ - width: 600, - height: 28, + options: { + scales: { + xAxes: [{ + id: 'xScale0', + type: 'category', + position: 'bottom' + }], + yAxes: [{ + id: 'yScale0', + type: 'linear' + }] + } + } }); - scale.left = 5; - scale.top = 5; - scale.right = 605; - scale.bottom = 33; + var xScale = chart.scales.xScale0; + expect(xScale.getPixelForValue(0, 0, 0, false)).toBeCloseToPixel(33); + expect(xScale.getPixelForValue(0, 0, 0, true)).toBeCloseToPixel(33); + expect(xScale.getValueForPixel(33)).toBe(0); - expect(scale.getPixelForValue(0, 0, 0, false)).toBe(33); - expect(scale.getPixelForValue(0, 0, 0, true)).toBe(85); - expect(scale.getValueForPixel(33)).toBe(0); - expect(scale.getValueForPixel(85)).toBe(0); + expect(xScale.getPixelForValue(0, 4, 0, false)).toBeCloseToPixel(487); + expect(xScale.getPixelForValue(0, 4, 0, true)).toBeCloseToPixel(487); + expect(xScale.getValueForPixel(487)).toBe(4); - expect(scale.getPixelForValue(0, 4, 0, false)).toBe(452); - expect(scale.getPixelForValue(0, 4, 0, true)).toBe(505); - expect(scale.getValueForPixel(453)).toBe(4); - expect(scale.getValueForPixel(505)).toBe(4); + xScale.options.gridLines.offsetGridLines = true; - config.gridLines.offsetGridLines = false; + expect(xScale.getPixelForValue(0, 0, 0, false)).toBeCloseToPixel(33); + expect(xScale.getPixelForValue(0, 0, 0, true)).toBeCloseToPixel(78); + expect(xScale.getValueForPixel(33)).toBe(0); + expect(xScale.getValueForPixel(78)).toBe(0); - expect(scale.getPixelForValue(0, 0, 0, false)).toBe(33); - expect(scale.getPixelForValue(0, 0, 0, true)).toBe(33); - expect(scale.getValueForPixel(33)).toBe(0); - - expect(scale.getPixelForValue(0, 4, 0, false)).toBe(557); - expect(scale.getPixelForValue(0, 4, 0, true)).toBe(557); - expect(scale.getValueForPixel(557)).toBe(4); + expect(xScale.getPixelForValue(0, 4, 0, false)).toBeCloseToPixel(396); + expect(xScale.getPixelForValue(0, 4, 0, true)).toBeCloseToPixel(441); + expect(xScale.getValueForPixel(397)).toBe(4); + expect(xScale.getValueForPixel(441)).toBe(4); }); it ('Should get the correct pixel for a value when there are repeated labels', function() { - var scaleID = 'myScale'; - - var mockData = { - datasets: [{ - yAxisID: scaleID, - data: [10, 5, 0, 25, 78] - }], - labels: ['tick1', 'tick1', 'tick3', 'tick3', 'tick_last'] - }; - - var mockContext = window.createMockContext(); - var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('category')); - config.gridLines.offsetGridLines = true; - var Constructor = Chart.scaleService.getScaleConstructor('category'); - var scale = new Constructor({ - ctx: mockContext, - options: config, - chart: { - data: mockData + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'xScale0', + yAxisID: 'yScale0', + data: [10, 5, 0, 25, 78] + }], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick_last'] }, - id: scaleID - }); - - var minSize = scale.update(600, 100); - - expect(scale.width).toBe(600); - expect(scale.height).toBe(28); - expect(scale.paddingTop).toBe(0); - expect(scale.paddingBottom).toBe(0); - expect(scale.paddingLeft).toBe(28); - expect(scale.paddingRight).toBe(48); - expect(scale.labelRotation).toBe(0); - - expect(minSize).toEqual({ - width: 600, - height: 28, + options: { + scales: { + xAxes: [{ + id: 'xScale0', + type: 'category', + position: 'bottom' + }], + yAxes: [{ + id: 'yScale0', + type: 'linear' + }] + } + } }); - scale.left = 5; - scale.top = 5; - scale.right = 605; - scale.bottom = 33; - - expect(scale.getPixelForValue('tick_1', 1, 0, false)).toBe(138); - expect(scale.getPixelForValue('tick_1', 1, 0, true)).toBe(190); + var xScale = chart.scales.xScale0; + expect(xScale.getPixelForValue('tick_1', 0, 0, false)).toBeCloseToPixel(33); + expect(xScale.getPixelForValue('tick_1', 1, 0, false)).toBeCloseToPixel(146); }); it ('Should get the correct pixel for a value when horizontal and zoomed', function() { - var scaleID = 'myScale'; - - var mockData = { - datasets: [{ - yAxisID: scaleID, - data: [10, 5, 0, 25, 78] - }], - labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick_last'] - }; - - var mockContext = window.createMockContext(); - var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('category')); - config.gridLines.offsetGridLines = true; - config.ticks.min = 'tick2'; - config.ticks.max = 'tick4'; - - var Constructor = Chart.scaleService.getScaleConstructor('category'); - var scale = new Constructor({ - ctx: mockContext, - options: config, - chart: { - data: mockData + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'xScale0', + yAxisID: 'yScale0', + data: [10, 5, 0, 25, 78] + }], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick_last'] }, - id: scaleID - }); - - var minSize = scale.update(600, 100); - - expect(scale.width).toBe(600); - expect(scale.height).toBe(28); - expect(scale.paddingTop).toBe(0); - expect(scale.paddingBottom).toBe(0); - expect(scale.paddingLeft).toBe(28); - expect(scale.paddingRight).toBe(28); - expect(scale.labelRotation).toBe(0); - - expect(minSize).toEqual({ - width: 600, - height: 28, + options: { + scales: { + xAxes: [{ + id: 'xScale0', + type: 'category', + position: 'bottom', + ticks: { + min: 'tick2', + max: 'tick4' + } + }], + yAxes: [{ + id: 'yScale0', + type: 'linear' + }] + } + } }); - scale.left = 5; - scale.top = 5; - scale.right = 605; - scale.bottom = 33; - - expect(scale.getPixelForValue(0, 1, 0, false)).toBe(33); - expect(scale.getPixelForValue(0, 1, 0, true)).toBe(124); + var xScale = chart.scales.xScale0; + expect(xScale.getPixelForValue(0, 1, 0, false)).toBeCloseToPixel(33); + expect(xScale.getPixelForValue(0, 1, 0, true)).toBeCloseToPixel(33); - expect(scale.getPixelForValue(0, 3, 0, false)).toBe(396); - expect(scale.getPixelForValue(0, 3, 0, true)).toBe(486); + expect(xScale.getPixelForValue(0, 3, 0, false)).toBeCloseToPixel(496); + expect(xScale.getPixelForValue(0, 3, 0, true)).toBeCloseToPixel(496); - config.gridLines.offsetGridLines = false; + xScale.options.gridLines.offsetGridLines = true; - expect(scale.getPixelForValue(0, 1, 0, false)).toBe(33); - expect(scale.getPixelForValue(0, 1, 0, true)).toBe(33); + expect(xScale.getPixelForValue(0, 1, 0, false)).toBeCloseToPixel(33); + expect(xScale.getPixelForValue(0, 1, 0, true)).toBeCloseToPixel(110); - expect(scale.getPixelForValue(0, 3, 0, false)).toBe(577); - expect(scale.getPixelForValue(0, 3, 0, true)).toBe(577); + expect(xScale.getPixelForValue(0, 3, 0, false)).toBeCloseToPixel(342); + expect(xScale.getPixelForValue(0, 3, 0, true)).toBeCloseToPixel(419); }); it ('should get the correct pixel for a value when vertical', function() { - var scaleID = 'myScale'; - - var mockData = { - datasets: [{ - yAxisID: scaleID, - data: [10, 5, 0, 25, 78] - }], - labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick_last'] - }; - - var mockContext = window.createMockContext(); - var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('category')); - config.gridLines.offsetGridLines = true; - config.position = 'left'; - var Constructor = Chart.scaleService.getScaleConstructor('category'); - var scale = new Constructor({ - ctx: mockContext, - options: config, - chart: { - data: mockData + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'xScale0', + yAxisID: 'yScale0', + data: ['3', '5', '1', '4', '2'] + }], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5'], + yLabels: ['1', '2', '3', '4', '5'] }, - id: scaleID - }); - - var minSize = scale.update(100, 200); - - expect(scale.width).toBe(100); - expect(scale.height).toBe(200); - expect(scale.paddingTop).toBe(6); - expect(scale.paddingBottom).toBe(6); - expect(scale.paddingLeft).toBe(0); - expect(scale.paddingRight).toBe(0); - expect(scale.labelRotation).toBe(0); - - expect(minSize).toEqual({ - width: 100, - height: 200, + options: { + scales: { + xAxes: [{ + id: 'xScale0', + type: 'category', + position: 'bottom', + }], + yAxes: [{ + id: 'yScale0', + type: 'category', + position: 'left' + }] + } + } }); - scale.left = 5; - scale.top = 5; - scale.right = 105; - scale.bottom = 205; + var yScale = chart.scales.yScale0; + expect(yScale.getPixelForValue(0, 0, 0, false)).toBe(32); + expect(yScale.getPixelForValue(0, 0, 0, true)).toBe(32); + expect(yScale.getValueForPixel(32)).toBe(0); - expect(scale.getPixelForValue(0, 0, 0, false)).toBe(11); - expect(scale.getPixelForValue(0, 0, 0, true)).toBe(30); - expect(scale.getValueForPixel(11)).toBe(0); - expect(scale.getValueForPixel(30)).toBe(0); + expect(yScale.getPixelForValue(0, 4, 0, false)).toBe(484); + expect(yScale.getPixelForValue(0, 4, 0, true)).toBe(484); + expect(yScale.getValueForPixel(484)).toBe(4); - expect(scale.getPixelForValue(0, 4, 0, false)).toBe(161); - expect(scale.getPixelForValue(0, 4, 0, true)).toBe(180); - expect(scale.getValueForPixel(162)).toBe(4); + yScale.options.gridLines.offsetGridLines = true; - config.gridLines.offsetGridLines = false; + expect(yScale.getPixelForValue(0, 0, 0, false)).toBe(32); + expect(yScale.getPixelForValue(0, 0, 0, true)).toBe(77); + expect(yScale.getValueForPixel(32)).toBe(0); + expect(yScale.getValueForPixel(77)).toBe(0); - expect(scale.getPixelForValue(0, 0, 0, false)).toBe(11); - expect(scale.getPixelForValue(0, 0, 0, true)).toBe(11); - expect(scale.getValueForPixel(11)).toBe(0); - - expect(scale.getPixelForValue(0, 4, 0, false)).toBe(199); - expect(scale.getPixelForValue(0, 4, 0, true)).toBe(199); - expect(scale.getValueForPixel(199)).toBe(4); + expect(yScale.getPixelForValue(0, 4, 0, false)).toBe(394); + expect(yScale.getPixelForValue(0, 4, 0, true)).toBe(439); + expect(yScale.getValueForPixel(394)).toBe(4); + expect(yScale.getValueForPixel(439)).toBe(4); }); it ('should get the correct pixel for a value when vertical and zoomed', function() { - var scaleID = 'myScale'; - - var mockData = { - datasets: [{ - yAxisID: scaleID, - data: [10, 5, 0, 25, 78] - }], - labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick_last'] - }; - - var mockContext = window.createMockContext(); - var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('category')); - config.gridLines.offsetGridLines = true; - config.ticks.min = 'tick2'; - config.ticks.max = 'tick4'; - config.position = 'left'; - - var Constructor = Chart.scaleService.getScaleConstructor('category'); - var scale = new Constructor({ - ctx: mockContext, - options: config, - chart: { - data: mockData + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'xScale0', + yAxisID: 'yScale0', + data: ['3', '5', '1', '4', '2'] + }], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5'], + yLabels: ['1', '2', '3', '4', '5'] }, - id: scaleID - }); - - var minSize = scale.update(100, 200); - - expect(scale.width).toBe(70); - expect(scale.height).toBe(200); - expect(scale.paddingTop).toBe(6); - expect(scale.paddingBottom).toBe(6); - expect(scale.paddingLeft).toBe(0); - expect(scale.paddingRight).toBe(0); - expect(scale.labelRotation).toBe(0); - - expect(minSize).toEqual({ - width: 70, - height: 200, + options: { + scales: { + xAxes: [{ + id: 'xScale0', + type: 'category', + position: 'bottom', + }], + yAxes: [{ + id: 'yScale0', + type: 'category', + position: 'left', + ticks: { + min: '2', + max: '4' + } + }] + } + } }); - scale.left = 5; - scale.top = 5; - scale.right = 75; - scale.bottom = 205; + var yScale = chart.scales.yScale0; - expect(scale.getPixelForValue(0, 1, 0, false)).toBe(11); - expect(scale.getPixelForValue(0, 1, 0, true)).toBe(42); + expect(yScale.getPixelForValue(0, 1, 0, false)).toBe(32); + expect(yScale.getPixelForValue(0, 1, 0, true)).toBe(32); - expect(scale.getPixelForValue(0, 3, 0, false)).toBe(136); - expect(scale.getPixelForValue(0, 3, 0, true)).toBe(168); + expect(yScale.getPixelForValue(0, 3, 0, false)).toBe(484); + expect(yScale.getPixelForValue(0, 3, 0, true)).toBe(484); - config.gridLines.offsetGridLines = false; + yScale.options.gridLines.offsetGridLines = true; - expect(scale.getPixelForValue(0, 1, 0, false)).toBe(11); - expect(scale.getPixelForValue(0, 1, 0, true)).toBe(11); + expect(yScale.getPixelForValue(0, 1, 0, false)).toBe(32); + expect(yScale.getPixelForValue(0, 1, 0, true)).toBe(107); - expect(scale.getPixelForValue(0, 3, 0, false)).toBe(199); - expect(scale.getPixelForValue(0, 3, 0, true)).toBe(199); + expect(yScale.getPixelForValue(0, 3, 0, false)).toBe(333); + expect(yScale.getPixelForValue(0, 3, 0, true)).toBe(409); }); }); From 4fbb1bdbbcfc25addf74cd6da3315c0a5f187011 Mon Sep 17 00:00:00 2001 From: Jerry Chang Date: Sun, 30 Oct 2016 17:34:06 -0700 Subject: [PATCH 13/61] Fixed Issue with tooltip label display when given null data value (#3528) When datasets.data contains a null value, the label displays incorrect value. code additions: - unit tests for truthy label values (when data is null) - checks to ensure handling of null value in getLabelByIndex method added mock data sets from issue #3528 example expect the return value from getLabelForIndex method to be valid (truthy) added check for null of first data value in getLabelForIndex fixed indentation and null comparison operator in code fixed mistake in definition of firstData variable changed testing for data on index 0 to using index variable changed firstData to use value instead condense the statments to use value variable --- .gitignore | 2 ++ src/scales/scale.time.js | 5 +++-- test/scale.time.tests.js | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 8ef0139ea96..172413437e6 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ .idea .vscode bower.json + +*.swp diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 0057dd92aea..61796c6ebcc 100755 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -360,9 +360,10 @@ module.exports = function(Chart) { getLabelForIndex: function(index, datasetIndex) { var me = this; var label = me.chart.data.labels && index < me.chart.data.labels.length ? me.chart.data.labels[index] : ''; + var value = me.chart.data.datasets[datasetIndex].data[index]; - if (typeof me.chart.data.datasets[datasetIndex].data[0] === 'object') { - label = me.getRightValue(me.chart.data.datasets[datasetIndex].data[index]); + if (value !== null && typeof value === 'object') { + label = me.getRightValue(value); } // Format nicely diff --git a/test/scale.time.tests.js b/test/scale.time.tests.js index 896ff00d4f7..d3d5d7b7f8b 100755 --- a/test/scale.time.tests.js +++ b/test/scale.time.tests.js @@ -428,7 +428,7 @@ describe('Time scale tests', function() { datasets: [{ xAxisID: 'xScale0', yAxisID: 'yScale0', - data: [] + data: [null, 10, 3] }], labels: ['2015-01-01T20:00:00', '2015-01-02T21:00:00', '2015-01-03T22:00:00', '2015-01-05T23:00:00', '2015-01-07T03:00', '2015-01-08T10:00', '2015-01-10T12:00'], // days }, @@ -449,6 +449,7 @@ describe('Time scale tests', function() { }); var xScale = chart.scales.xScale0; + expect(xScale.getLabelForIndex(0, 0)).toBeTruthy(); expect(xScale.getLabelForIndex(0, 0)).toBe('2015-01-01T20:00:00'); expect(xScale.getLabelForIndex(6, 0)).toBe('2015-01-10T12:00'); }); From 58afbe428c74d3f2df2df5620f94c34dce3798d2 Mon Sep 17 00:00:00 2001 From: etimberg Date: Fri, 4 Nov 2016 20:41:54 -0400 Subject: [PATCH 14/61] Properly use the ticks.padding option. To correctly fix the issue, the default padding was changed from 0 to 10. This change caused all of the test changes since the width of a vertical scale was lowered by 10px --- src/core/core.scale.js | 27 ++++++------- test/controller.bar.tests.js | 40 +++++++++---------- test/controller.bubble.tests.js | 6 +-- test/controller.line.tests.js | 66 ++++++++++++++++---------------- test/core.helpers.tests.js | 4 +- test/core.tooltip.tests.js | 8 ++-- test/scale.category.tests.js | 26 ++++++------- test/scale.linear.tests.js | 18 ++++----- test/scale.logarithmic.tests.js | 12 +++--- test/scale.radialLinear.tests.js | 2 +- test/scale.time.tests.js | 8 ++-- 11 files changed, 107 insertions(+), 110 deletions(-) diff --git a/src/core/core.scale.js b/src/core/core.scale.js index 32ece041f5b..ce399219bfb 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -39,7 +39,7 @@ module.exports = function(Chart) { minRotation: 0, maxRotation: 50, mirror: false, - padding: 10, + padding: 0, reverse: false, display: true, autoSkip: true, @@ -609,23 +609,20 @@ module.exports = function(Chart) { y1 = chartArea.top; y2 = chartArea.bottom; } else { - if (options.position === 'left') { - if (optionTicks.mirror) { - labelX = me.right + optionTicks.padding; - textAlign = 'left'; - } else { - labelX = me.right - optionTicks.padding; - textAlign = 'right'; - } - // right side - } else if (optionTicks.mirror) { - labelX = me.left - optionTicks.padding; - textAlign = 'right'; + var isLeft = options.position === 'left'; + var tickPadding = optionTicks.padding; + var labelXOffset; + + if (optionTicks.mirror) { + textAlign = isLeft ? 'left' : 'right'; + labelXOffset = tickPadding; } else { - labelX = me.left + optionTicks.padding; - textAlign = 'left'; + textAlign = isLeft ? 'right' : 'left'; + labelXOffset = tl + tickPadding; } + labelX = isLeft ? me.right - labelXOffset : me.left + labelXOffset; + var yLineValue = me.getPixelForTick(index); // xvalues for grid lines yLineValue += helpers.aliasPixel(lineWidth); labelY = me.getPixelForTick(index, gridLines.offsetGridLines); diff --git a/test/controller.bar.tests.js b/test/controller.bar.tests.js index a77eea22ead..15fa00afed9 100644 --- a/test/controller.bar.tests.js +++ b/test/controller.bar.tests.js @@ -155,8 +155,8 @@ describe('Bar controller tests', function() { expect(meta.data.length).toBe(2); [ - {x: 122, y: 484}, - {x: 234, y: 32} + {x: 113, y: 484}, + {x: 229, y: 32} ].forEach(function(expected, i) { expect(meta.data[i]._datasetIndex).toBe(1); expect(meta.data[i]._index).toBe(i); @@ -217,9 +217,9 @@ describe('Bar controller tests', function() { var bar1 = meta.data[0]; var bar2 = meta.data[1]; - expect(bar1._model.x).toBeCloseToPixel(194); + expect(bar1._model.x).toBeCloseToPixel(187); expect(bar1._model.y).toBeCloseToPixel(132); - expect(bar2._model.x).toBeCloseToPixel(424); + expect(bar2._model.x).toBeCloseToPixel(422); expect(bar2._model.y).toBeCloseToPixel(32); }); @@ -253,10 +253,10 @@ describe('Bar controller tests', function() { var meta0 = chart.getDatasetMeta(0); [ - {b: 290, w: 91, x: 95, y: 161}, - {b: 290, w: 91, x: 209, y: 419}, - {b: 290, w: 91, x: 322, y: 161}, - {b: 290, w: 91, x: 436, y: 419} + {b: 290, w: 93, x: 86, y: 161}, + {b: 290, w: 93, x: 202, y: 419}, + {b: 290, w: 93, x: 318, y: 161}, + {b: 290, w: 93, x: 436, y: 419} ].forEach(function(values, i) { expect(meta0.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta0.data[i]._model.width).toBeCloseToPixel(values.w); @@ -267,10 +267,10 @@ describe('Bar controller tests', function() { var meta1 = chart.getDatasetMeta(1); [ - {b: 161, w: 91, x: 95, y: 32}, - {b: 290, w: 91, x: 209, y: 97}, - {b: 161, w: 91, x: 322, y: 161}, - {b: 419, w: 91, x: 436, y: 471} + {b: 161, w: 93, x: 86, y: 32}, + {b: 290, w: 93, x: 202, y: 97}, + {b: 161, w: 93, x: 318, y: 161}, + {b: 419, w: 93, x: 436, y: 471} ].forEach(function(values, i) { expect(meta1.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta1.data[i]._model.width).toBeCloseToPixel(values.w); @@ -309,10 +309,10 @@ describe('Bar controller tests', function() { var meta0 = chart.getDatasetMeta(0); [ - {b: 290, w: 91, x: 95, y: 161}, - {b: 290, w: 91, x: 209, y: 419}, - {b: 290, w: 91, x: 322, y: 161}, - {b: 290, w: 91, x: 436, y: 419} + {b: 290, w: 93, x: 86, y: 161}, + {b: 290, w: 93, x: 202, y: 419}, + {b: 290, w: 93, x: 318, y: 161}, + {b: 290, w: 93, x: 436, y: 419} ].forEach(function(values, i) { expect(meta0.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta0.data[i]._model.width).toBeCloseToPixel(values.w); @@ -323,10 +323,10 @@ describe('Bar controller tests', function() { var meta1 = chart.getDatasetMeta(1); [ - {b: 161, w: 91, x: 95, y: 32}, - {b: 290, w: 91, x: 209, y: 97}, - {b: 161, w: 91, x: 322, y: 161}, - {b: 419, w: 91, x: 436, y: 471} + {b: 161, w: 93, x: 86, y: 32}, + {b: 290, w: 93, x: 202, y: 97}, + {b: 161, w: 93, x: 318, y: 161}, + {b: 419, w: 93, x: 436, y: 471} ].forEach(function(values, i) { expect(meta1.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta1.data[i]._model.width).toBeCloseToPixel(values.w); diff --git a/test/controller.bubble.tests.js b/test/controller.bubble.tests.js index 977b2d517b1..4914e1005b0 100644 --- a/test/controller.bubble.tests.js +++ b/test/controller.bubble.tests.js @@ -134,9 +134,9 @@ describe('Bubble controller tests', function() { var meta = chart.getDatasetMeta(0); [ - {r: 5, x: 38, y: 32}, - {r: 1, x: 189, y: 484}, - {r: 2, x: 341, y: 461}, + {r: 5, x: 28, y: 32}, + {r: 1, x: 183, y: 484}, + {r: 2, x: 338, y: 461}, {r: 1, x: 492, y: 32} ].forEach(function(expected, i) { expect(meta.data[i]._model.radius).toBe(expected.r); diff --git a/test/controller.line.tests.js b/test/controller.line.tests.js index f15c693703b..2341b403cd8 100644 --- a/test/controller.line.tests.js +++ b/test/controller.line.tests.js @@ -203,8 +203,8 @@ describe('Line controller tests', function() { [ - {x: 44, y: 484}, - {x: 193, y: 32} + {x: 33, y: 484}, + {x: 186, y: 32} ].forEach(function(expected, i) { expect(meta.data[i]._datasetIndex).toBe(0); expect(meta.data[i]._index).toBe(i); @@ -250,7 +250,7 @@ describe('Line controller tests', function() { var meta = chart.getDatasetMeta(0); // 1 point var point = meta.data[0]; - expect(point._model.x).toBeCloseToPixel(267); + expect(point._model.x).toBeCloseToPixel(262); // 2 points chart.data.labels = ['One', 'Two']; @@ -259,7 +259,7 @@ describe('Line controller tests', function() { var points = meta.data; - expect(points[0]._model.x).toBeCloseToPixel(37); + expect(points[0]._model.x).toBeCloseToPixel(27); expect(points[1]._model.x).toBeCloseToPixel(498); // 3 points @@ -269,8 +269,8 @@ describe('Line controller tests', function() { points = meta.data; - expect(points[0]._model.x).toBeCloseToPixel(37); - expect(points[1]._model.x).toBeCloseToPixel(265); + expect(points[0]._model.x).toBeCloseToPixel(27); + expect(points[1]._model.x).toBeCloseToPixel(260); expect(points[2]._model.x).toBeCloseToPixel(493); // 4 points @@ -280,9 +280,9 @@ describe('Line controller tests', function() { points = meta.data; - expect(points[0]._model.x).toBeCloseToPixel(37); - expect(points[1]._model.x).toBeCloseToPixel(190); - expect(points[2]._model.x).toBeCloseToPixel(343); + expect(points[0]._model.x).toBeCloseToPixel(27); + expect(points[1]._model.x).toBeCloseToPixel(184); + expect(points[2]._model.x).toBeCloseToPixel(340); expect(points[3]._model.x).toBeCloseToPixel(497); }); @@ -311,9 +311,9 @@ describe('Line controller tests', function() { var meta0 = chart.getDatasetMeta(0); [ - {x: 38, y: 161}, - {x: 189, y: 419}, - {x: 341, y: 161}, + {x: 28, y: 161}, + {x: 183, y: 419}, + {x: 338, y: 161}, {x: 492, y: 419} ].forEach(function(values, i) { expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); @@ -323,9 +323,9 @@ describe('Line controller tests', function() { var meta1 = chart.getDatasetMeta(1); [ - {x: 38, y: 32}, - {x: 189, y: 97}, - {x: 341, y: 161}, + {x: 28, y: 32}, + {x: 183, y: 97}, + {x: 338, y: 161}, {x: 492, y: 471} ].forEach(function(values, i) { expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); @@ -366,9 +366,9 @@ describe('Line controller tests', function() { var meta0 = chart.getDatasetMeta(0); [ - {x: 76, y: 161}, - {x: 215, y: 419}, - {x: 353, y: 161}, + {x: 56, y: 161}, + {x: 202, y: 419}, + {x: 347, y: 161}, {x: 492, y: 419} ].forEach(function(values, i) { expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); @@ -378,9 +378,9 @@ describe('Line controller tests', function() { var meta1 = chart.getDatasetMeta(1); [ - {x: 76, y: 32}, - {x: 215, y: 97}, - {x: 353, y: 161}, + {x: 56, y: 32}, + {x: 202, y: 97}, + {x: 347, y: 161}, {x: 492, y: 471} ].forEach(function(values, i) { expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); @@ -438,9 +438,9 @@ describe('Line controller tests', function() { var meta0 = chart.getDatasetMeta(0); [ - {x: 38, y: 161}, - {x: 189, y: 419}, - {x: 341, y: 161}, + {x: 28, y: 161}, + {x: 183, y: 419}, + {x: 338, y: 161}, {x: 492, y: 419} ].forEach(function(values, i) { expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); @@ -450,9 +450,9 @@ describe('Line controller tests', function() { var meta1 = chart.getDatasetMeta(1); [ - {x: 38, y: 32}, - {x: 189, y: 97}, - {x: 341, y: 161}, + {x: 28, y: 32}, + {x: 183, y: 97}, + {x: 338, y: 161}, {x: 492, y: 471} ].forEach(function(values, i) { expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); @@ -486,9 +486,9 @@ describe('Line controller tests', function() { var meta0 = chart.getDatasetMeta(0); [ - {x: 38, y: 161}, - {x: 189, y: 419}, - {x: 341, y: 161}, + {x: 28, y: 161}, + {x: 183, y: 419}, + {x: 338, y: 161}, {x: 492, y: 419} ].forEach(function(values, i) { expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); @@ -498,9 +498,9 @@ describe('Line controller tests', function() { var meta1 = chart.getDatasetMeta(1); [ - {x: 38, y: 32}, - {x: 189, y: 97}, - {x: 341, y: 161}, + {x: 28, y: 32}, + {x: 183, y: 97}, + {x: 338, y: 161}, {x: 492, y: 471} ].forEach(function(values, i) { expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); diff --git a/test/core.helpers.tests.js b/test/core.helpers.tests.js index 4a7e9f555f1..4cd200ae3b8 100644 --- a/test/core.helpers.tests.js +++ b/test/core.helpers.tests.js @@ -206,7 +206,7 @@ describe('Core helper tests', function() { minRotation: 0, maxRotation: 50, mirror: false, - padding: 10, + padding: 0, reverse: false, display: true, callback: merged.scales.yAxes[1].ticks.callback, // make it nicer, then check explicitly below @@ -242,7 +242,7 @@ describe('Core helper tests', function() { minRotation: 0, maxRotation: 50, mirror: false, - padding: 10, + padding: 0, reverse: false, display: true, callback: merged.scales.yAxes[2].ticks.callback, // make it nicer, then check explicitly below diff --git a/test/core.tooltip.tests.js b/test/core.tooltip.tests.js index 1a2d8fb0bbc..d96dfa302d4 100755 --- a/test/core.tooltip.tests.js +++ b/test/core.tooltip.tests.js @@ -113,7 +113,7 @@ describe('Core.Tooltip', function() { }] })); - expect(tooltip._view.x).toBeCloseToPixel(269); + expect(tooltip._view.x).toBeCloseToPixel(263); expect(tooltip._view.y).toBeCloseToPixel(155); }); @@ -310,7 +310,7 @@ describe('Core.Tooltip', function() { }] })); - expect(tooltip._view.x).toBeCloseToPixel(269); + expect(tooltip._view.x).toBeCloseToPixel(263); expect(tooltip._view.y).toBeCloseToPixel(312); }); @@ -459,7 +459,7 @@ describe('Core.Tooltip', function() { }] })); - expect(tooltip._view.x).toBeCloseToPixel(216); + expect(tooltip._view.x).toBeCloseToPixel(211); expect(tooltip._view.y).toBeCloseToPixel(190); }); @@ -539,7 +539,7 @@ describe('Core.Tooltip', function() { }] })); - expect(tooltip._view.x).toBeCloseToPixel(269); + expect(tooltip._view.x).toBeCloseToPixel(263); expect(tooltip._view.y).toBeCloseToPixel(155); }); diff --git a/test/scale.category.tests.js b/test/scale.category.tests.js index 08cae8ba0b4..c474dd9c7fd 100644 --- a/test/scale.category.tests.js +++ b/test/scale.category.tests.js @@ -36,7 +36,7 @@ describe('Category scale tests', function() { minRotation: 0, maxRotation: 50, mirror: false, - padding: 10, + padding: 0, reverse: false, display: true, callback: defaultConfig.ticks.callback, // make this nicer, then check explicitly below @@ -187,8 +187,8 @@ describe('Category scale tests', function() { }); var xScale = chart.scales.xScale0; - expect(xScale.getPixelForValue(0, 0, 0, false)).toBeCloseToPixel(33); - expect(xScale.getPixelForValue(0, 0, 0, true)).toBeCloseToPixel(33); + expect(xScale.getPixelForValue(0, 0, 0, false)).toBeCloseToPixel(23); + expect(xScale.getPixelForValue(0, 0, 0, true)).toBeCloseToPixel(23); expect(xScale.getValueForPixel(33)).toBe(0); expect(xScale.getPixelForValue(0, 4, 0, false)).toBeCloseToPixel(487); @@ -197,12 +197,12 @@ describe('Category scale tests', function() { xScale.options.gridLines.offsetGridLines = true; - expect(xScale.getPixelForValue(0, 0, 0, false)).toBeCloseToPixel(33); - expect(xScale.getPixelForValue(0, 0, 0, true)).toBeCloseToPixel(78); + expect(xScale.getPixelForValue(0, 0, 0, false)).toBeCloseToPixel(23); + expect(xScale.getPixelForValue(0, 0, 0, true)).toBeCloseToPixel(69); expect(xScale.getValueForPixel(33)).toBe(0); expect(xScale.getValueForPixel(78)).toBe(0); - expect(xScale.getPixelForValue(0, 4, 0, false)).toBeCloseToPixel(396); + expect(xScale.getPixelForValue(0, 4, 0, false)).toBeCloseToPixel(395); expect(xScale.getPixelForValue(0, 4, 0, true)).toBeCloseToPixel(441); expect(xScale.getValueForPixel(397)).toBe(4); expect(xScale.getValueForPixel(441)).toBe(4); @@ -235,8 +235,8 @@ describe('Category scale tests', function() { }); var xScale = chart.scales.xScale0; - expect(xScale.getPixelForValue('tick_1', 0, 0, false)).toBeCloseToPixel(33); - expect(xScale.getPixelForValue('tick_1', 1, 0, false)).toBeCloseToPixel(146); + expect(xScale.getPixelForValue('tick_1', 0, 0, false)).toBeCloseToPixel(23); + expect(xScale.getPixelForValue('tick_1', 1, 0, false)).toBeCloseToPixel(139); }); it ('Should get the correct pixel for a value when horizontal and zoomed', function() { @@ -270,18 +270,18 @@ describe('Category scale tests', function() { }); var xScale = chart.scales.xScale0; - expect(xScale.getPixelForValue(0, 1, 0, false)).toBeCloseToPixel(33); - expect(xScale.getPixelForValue(0, 1, 0, true)).toBeCloseToPixel(33); + expect(xScale.getPixelForValue(0, 1, 0, false)).toBeCloseToPixel(23); + expect(xScale.getPixelForValue(0, 1, 0, true)).toBeCloseToPixel(23); expect(xScale.getPixelForValue(0, 3, 0, false)).toBeCloseToPixel(496); expect(xScale.getPixelForValue(0, 3, 0, true)).toBeCloseToPixel(496); xScale.options.gridLines.offsetGridLines = true; - expect(xScale.getPixelForValue(0, 1, 0, false)).toBeCloseToPixel(33); - expect(xScale.getPixelForValue(0, 1, 0, true)).toBeCloseToPixel(110); + expect(xScale.getPixelForValue(0, 1, 0, false)).toBeCloseToPixel(23); + expect(xScale.getPixelForValue(0, 1, 0, true)).toBeCloseToPixel(102); - expect(xScale.getPixelForValue(0, 3, 0, false)).toBeCloseToPixel(342); + expect(xScale.getPixelForValue(0, 3, 0, false)).toBeCloseToPixel(338); expect(xScale.getPixelForValue(0, 3, 0, true)).toBeCloseToPixel(419); }); diff --git a/test/scale.linear.tests.js b/test/scale.linear.tests.js index 1e18fb529fb..44c4dcf29e3 100644 --- a/test/scale.linear.tests.js +++ b/test/scale.linear.tests.js @@ -34,7 +34,7 @@ describe('Linear Scale', function() { minRotation: 0, maxRotation: 50, mirror: false, - padding: 10, + padding: 0, reverse: false, display: true, callback: defaultConfig.ticks.callback, // make this work nicer, then check below @@ -660,12 +660,12 @@ describe('Linear Scale', function() { var xScale = chart.scales.xScale0; expect(xScale.getPixelForValue(1, 0, 0)).toBeCloseToPixel(501); // right - paddingRight - expect(xScale.getPixelForValue(-1, 0, 0)).toBeCloseToPixel(41); // left + paddingLeft - expect(xScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(271); // halfway*/ + expect(xScale.getPixelForValue(-1, 0, 0)).toBeCloseToPixel(31); // left + paddingLeft + expect(xScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(266); // halfway*/ expect(xScale.getValueForPixel(501)).toBeCloseTo(1, 1e-2); - expect(xScale.getValueForPixel(41)).toBeCloseTo(-1, 1e-2); - expect(xScale.getValueForPixel(271)).toBeCloseTo(0, 1e-2); + expect(xScale.getValueForPixel(31)).toBeCloseTo(-1, 1e-2); + expect(xScale.getValueForPixel(266)).toBeCloseTo(0, 1e-2); var yScale = chart.scales.yScale0; expect(yScale.getPixelForValue(1, 0, 0)).toBeCloseToPixel(32); // right - paddingRight @@ -719,7 +719,7 @@ describe('Linear Scale', function() { expect(xScale.paddingBottom).toBeCloseToPixel(0); expect(xScale.paddingLeft).toBeCloseToPixel(0); expect(xScale.paddingRight).toBeCloseToPixel(0); - expect(xScale.width).toBeCloseToPixel(457.5); + expect(xScale.width).toBeCloseToPixel(468); expect(xScale.height).toBeCloseToPixel(28); var yScale = chart.scales.yScale0; @@ -727,7 +727,7 @@ describe('Linear Scale', function() { expect(yScale.paddingBottom).toBeCloseToPixel(0); expect(yScale.paddingLeft).toBeCloseToPixel(0); expect(yScale.paddingRight).toBeCloseToPixel(0); - expect(yScale.width).toBeCloseToPixel(41); + expect(yScale.width).toBeCloseToPixel(30); expect(yScale.height).toBeCloseToPixel(452); // Extra size when scale label showing @@ -739,14 +739,14 @@ describe('Linear Scale', function() { expect(xScale.paddingBottom).toBeCloseToPixel(0); expect(xScale.paddingLeft).toBeCloseToPixel(0); expect(xScale.paddingRight).toBeCloseToPixel(0); - expect(xScale.width).toBeCloseToPixel(439.5); + expect(xScale.width).toBeCloseToPixel(450); expect(xScale.height).toBeCloseToPixel(46); expect(yScale.paddingTop).toBeCloseToPixel(0); expect(yScale.paddingBottom).toBeCloseToPixel(0); expect(yScale.paddingLeft).toBeCloseToPixel(0); expect(yScale.paddingRight).toBeCloseToPixel(0); - expect(yScale.width).toBeCloseToPixel(59); + expect(yScale.width).toBeCloseToPixel(48); expect(yScale.height).toBeCloseToPixel(434); }); diff --git a/test/scale.logarithmic.tests.js b/test/scale.logarithmic.tests.js index 14a382e5f76..64732d833b5 100644 --- a/test/scale.logarithmic.tests.js +++ b/test/scale.logarithmic.tests.js @@ -33,7 +33,7 @@ describe('Logarithmic Scale tests', function() { minRotation: 0, maxRotation: 50, mirror: false, - padding: 10, + padding: 0, reverse: false, display: true, callback: defaultConfig.ticks.callback, // make this nicer, then check explicitly below @@ -717,14 +717,14 @@ describe('Logarithmic Scale tests', function() { }); var xScale = chart.scales.xScale; - expect(xScale.getPixelForValue(80, 0, 0)).toBeCloseToPixel(481.5); // right - paddingRight - expect(xScale.getPixelForValue(1, 0, 0)).toBeCloseToPixel(48); // left + paddingLeft - expect(xScale.getPixelForValue(10, 0, 0)).toBeCloseToPixel(276); // halfway - expect(xScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(48); // 0 is invalid, put it on the left. + expect(xScale.getPixelForValue(80, 0, 0)).toBeCloseToPixel(482); // right - paddingRight + expect(xScale.getPixelForValue(1, 0, 0)).toBeCloseToPixel(37); // left + paddingLeft + expect(xScale.getPixelForValue(10, 0, 0)).toBeCloseToPixel(270); // halfway + expect(xScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(37); // 0 is invalid, put it on the left. expect(xScale.getValueForPixel(481.5)).toBeCloseToPixel(80); expect(xScale.getValueForPixel(48)).toBeCloseTo(1, 1e-4); - expect(xScale.getValueForPixel(276)).toBeCloseTo(10, 1e-4); + expect(xScale.getValueForPixel(270)).toBeCloseTo(10, 1e-4); var yScale = chart.scales.yScale; expect(yScale.getPixelForValue(80, 0, 0)).toBeCloseToPixel(32); // top + paddingTop diff --git a/test/scale.radialLinear.tests.js b/test/scale.radialLinear.tests.js index 7ac95ac39a6..b4004f1bafe 100644 --- a/test/scale.radialLinear.tests.js +++ b/test/scale.radialLinear.tests.js @@ -48,7 +48,7 @@ describe('Test the radial linear scale', function() { minRotation: 0, maxRotation: 50, mirror: false, - padding: 10, + padding: 0, reverse: false, showLabelBackdrop: true, display: true, diff --git a/test/scale.time.tests.js b/test/scale.time.tests.js index d3d5d7b7f8b..ba2ef2f8a2b 100755 --- a/test/scale.time.tests.js +++ b/test/scale.time.tests.js @@ -58,7 +58,7 @@ describe('Time scale tests', function() { minRotation: 0, maxRotation: 50, mirror: false, - padding: 10, + padding: 0, reverse: false, display: true, callback: defaultConfig.ticks.callback, // make this nicer, then check explicitly below, @@ -406,11 +406,11 @@ describe('Time scale tests', function() { var xScale = chart.scales.xScale0; - expect(xScale.getPixelForValue('', 0, 0)).toBeCloseToPixel(78); + expect(xScale.getPixelForValue('', 0, 0)).toBeCloseToPixel(71); expect(xScale.getPixelForValue('', 6, 0)).toBeCloseToPixel(452); - expect(xScale.getPixelForValue('2015-01-01T20:00:00')).toBeCloseToPixel(78); + expect(xScale.getPixelForValue('2015-01-01T20:00:00')).toBeCloseToPixel(71); - expect(xScale.getValueForPixel(78)).toBeCloseToTime({ + expect(xScale.getValueForPixel(71)).toBeCloseToTime({ value: moment(chart.data.labels[0]), unit: 'hour', threshold: 0.75 From 68b00b2dc512b9041037c5f87fc28fcc7ac5b9fa Mon Sep 17 00:00:00 2001 From: etimberg Date: Fri, 4 Nov 2016 21:31:00 -0400 Subject: [PATCH 15/61] Labels can get bigger when the 2nd fit happens. Don't arbitrarily force the size to change --- src/core/core.scale.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/core/core.scale.js b/src/core/core.scale.js index ce399219bfb..bc09cb0ae7b 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -353,7 +353,6 @@ module.exports = function(Chart) { me.paddingRight = me.labelRotation !== 0 ? (sinRotation * (tickFontSize / 2)) + 3 : lastLabelWidth / 2 + 3; // when rotated } else { // A vertical axis is more constrained by the width. Labels are the dominant factor here, so get that length first - var maxLabelWidth = me.maxWidth - minSize.width; // Account for padding var mirror = tickOpts.mirror; @@ -364,14 +363,7 @@ module.exports = function(Chart) { largestTextWidth = 0; } - if (largestTextWidth < maxLabelWidth) { - // We don't need all the room - minSize.width += largestTextWidth; - } else { - // Expand to max size - minSize.width = me.maxWidth; - } - + minSize.width += largestTextWidth; me.paddingTop = tickFontSize / 2; me.paddingBottom = tickFontSize / 2; } From fe68b7760656bef8e47d216b8579cabe6e93ea60 Mon Sep 17 00:00:00 2001 From: Jeff Carey Date: Tue, 22 Nov 2016 14:51:22 -0800 Subject: [PATCH 16/61] Upgraded dependency gulp-uglify to 2.0.0 (#3635) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b8dba18746a..762f233d7c5 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "gulp-replace": "^0.5.4", "gulp-size": "~0.4.0", "gulp-streamify": "^1.0.2", - "gulp-uglify": "~0.2.x", + "gulp-uglify": "~2.0.x", "gulp-util": "~2.2.x", "gulp-zip": "~3.2.0", "jasmine": "^2.3.2", From 7e5e29e3ee15100de599124b4951d50f3aad5f57 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Fri, 25 Nov 2016 07:19:43 -0500 Subject: [PATCH 17/61] Improve radial scale (#3625) Clean up radial linear scale. It now supports multiple lines for point labels. Fixes #3225 --- samples/radar/radar.html | 2 +- src/scales/scale.radialLinear.js | 505 +++++++++++++++++------------ test/controller.polarArea.tests.js | 14 +- test/scale.radialLinear.tests.js | 18 +- 4 files changed, 318 insertions(+), 221 deletions(-) diff --git a/samples/radar/radar.html b/samples/radar/radar.html index 507d5bb47d7..586e2df7aec 100644 --- a/samples/radar/radar.html +++ b/samples/radar/radar.html @@ -32,7 +32,7 @@ var config = { type: 'radar', data: { - labels: ["Eating", "Drinking", "Sleeping", "Designing", "Coding", "Cycling", "Running"], + labels: [["Eating", "Dinner"], ["Drinking", "Water"], "Sleeping", ["Designing", "Graphics"], "Coding", "Cycling", "Running"], datasets: [{ label: "My First dataset", backgroundColor: color(window.chartColors.red).alpha(0.2).rgbString(), diff --git a/src/scales/scale.radialLinear.js b/src/scales/scale.radialLinear.js index cc7c6557d74..b51fa698bd9 100644 --- a/src/scales/scale.radialLinear.js +++ b/src/scales/scale.radialLinear.js @@ -47,10 +47,266 @@ module.exports = function(Chart) { } }; + function getValueCount(scale) { + return !scale.options.lineArc ? scale.chart.data.labels.length : 0; + } + + function getPointLabelFontOptions(scale) { + var pointLabelOptions = scale.options.pointLabels; + var fontSize = helpers.getValueOrDefault(pointLabelOptions.fontSize, globalDefaults.defaultFontSize); + var fontStyle = helpers.getValueOrDefault(pointLabelOptions.fontStyle, globalDefaults.defaultFontStyle); + var fontFamily = helpers.getValueOrDefault(pointLabelOptions.fontFamily, globalDefaults.defaultFontFamily); + var font = helpers.fontString(fontSize, fontStyle, fontFamily); + + return { + size: fontSize, + style: fontStyle, + family: fontFamily, + font: font + }; + } + + function measureLabelSize(ctx, fontSize, label) { + if (helpers.isArray(label)) { + return { + w: helpers.longestText(ctx, ctx.font, label), + h: (label.length * fontSize) + ((label.length - 1) * 1.5 * fontSize) + }; + } + + return { + w: ctx.measureText(label).width, + h: fontSize + }; + } + + function determineLimits(angle, pos, size, min, max) { + if (angle === min || angle === max) { + return { + start: pos - (size / 2), + end: pos + (size / 2) + }; + } else if (angle < min || angle > max) { + return { + start: pos - size - 5, + end: pos + }; + } + + return { + start: pos, + end: pos + size + 5 + }; + } + + /** + * Helper function to fit a radial linear scale with point labels + */ + function fitWithPointLabels(scale) { + /* + * Right, this is really confusing and there is a lot of maths going on here + * The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9 + * + * Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif + * + * Solution: + * + * We assume the radius of the polygon is half the size of the canvas at first + * at each index we check if the text overlaps. + * + * Where it does, we store that angle and that index. + * + * After finding the largest index and angle we calculate how much we need to remove + * from the shape radius to move the point inwards by that x. + * + * We average the left and right distances to get the maximum shape radius that can fit in the box + * along with labels. + * + * Once we have that, we can find the centre point for the chart, by taking the x text protrusion + * on each side, removing that from the size, halving it and adding the left x protrusion width. + * + * This will mean we have a shape fitted to the canvas, as large as it can be with the labels + * and position it in the most space efficient manner + * + * https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif + */ + + var plFont = getPointLabelFontOptions(scale); + + // Get maximum radius of the polygon. Either half the height (minus the text width) or half the width. + // Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points + var largestPossibleRadius = Math.min(scale.height / 2, scale.width / 2); + var furthestLimits = { + l: scale.width, + r: 0, + t: scale.height, + b: 0 + }; + var furthestAngles = {}; + var i; + var textSize; + var pointPosition; + + scale.ctx.font = plFont.font; + scale._pointLabelSizes = []; + + var valueCount = getValueCount(scale); + for (i = 0; i < valueCount; i++) { + pointPosition = scale.getPointPosition(i, largestPossibleRadius); + textSize = measureLabelSize(scale.ctx, plFont.size, scale.pointLabels[i] || ''); + scale._pointLabelSizes[i] = textSize; + + // Add quarter circle to make degree 0 mean top of circle + var angleRadians = scale.getIndexAngle(i); + var angle = helpers.toDegrees(angleRadians) % 360; + var hLimits = determineLimits(angle, pointPosition.x, textSize.w, 0, 180); + var vLimits = determineLimits(angle, pointPosition.y, textSize.h, 90, 270); + + if (hLimits.start < furthestLimits.l) { + furthestLimits.l = hLimits.start; + furthestAngles.l = angleRadians; + } + + if (hLimits.end > furthestLimits.r) { + furthestLimits.r = hLimits.end; + furthestAngles.r = angleRadians; + } + + if (vLimits.start < furthestLimits.t) { + furthestLimits.t = vLimits.start; + furthestAngles.t = angleRadians; + } + + if (vLimits.end > furthestLimits.b) { + furthestLimits.b = vLimits.end; + furthestAngles.b = angleRadians; + } + } + + scale.setReductions(largestPossibleRadius, furthestLimits, furthestAngles); + } + + /** + * Helper function to fit a radial linear scale with no point labels + */ + function fit(scale) { + var largestPossibleRadius = Math.min(scale.height / 2, scale.width / 2); + scale.drawingArea = Math.round(largestPossibleRadius); + scale.setCenterPoint(0, 0, 0, 0); + } + + function getTextAlignForAngle(angle) { + if (angle === 0 || angle === 180) { + return 'center'; + } else if (angle < 180) { + return 'left'; + } + + return 'right'; + } + + function fillText(ctx, text, position, fontSize) { + if (helpers.isArray(text)) { + var y = position.y; + var spacing = 1.5 * fontSize; + + for (var i = 0; i < text.length; ++i) { + ctx.fillText(text[i], position.x, y); + y+= spacing; + } + } else { + ctx.fillText(text, position.x, position.y); + } + } + + function adjustPointPositionForLabelHeight(angle, textSize, position) { + if (angle === 90 || angle === 270) { + position.y -= (textSize.h / 2); + } else if (angle > 270 || angle < 90) { + position.y -= textSize.h; + } + } + + function drawPointLabels(scale) { + var ctx = scale.ctx; + var getValueOrDefault = helpers.getValueOrDefault; + var opts = scale.options; + var angleLineOpts = opts.angleLines; + var pointLabelOpts = opts.pointLabels; + + ctx.lineWidth = angleLineOpts.lineWidth; + ctx.strokeStyle = angleLineOpts.color; + + var outerDistance = scale.getDistanceFromCenterForValue(opts.reverse ? scale.min : scale.max); + + // Point Label Font + var plFont = getPointLabelFontOptions(scale); + + ctx.textBaseline = 'top'; + + for (var i = getValueCount(scale) - 1; i >= 0; i--) { + if (angleLineOpts.display) { + var outerPosition = scale.getPointPosition(i, outerDistance); + ctx.beginPath(); + ctx.moveTo(scale.xCenter, scale.yCenter); + ctx.lineTo(outerPosition.x, outerPosition.y); + ctx.stroke(); + ctx.closePath(); + } + // Extra 3px out for some label spacing + var pointLabelPosition = scale.getPointPosition(i, outerDistance + 5); + + // Keep this in loop since we may support array properties here + var pointLabelFontColor = getValueOrDefault(pointLabelOpts.fontColor, globalDefaults.defaultFontColor); + ctx.font = plFont.font; + ctx.fillStyle = pointLabelFontColor; + + var angleRadians = scale.getIndexAngle(i); + var angle = helpers.toDegrees(angleRadians); + ctx.textAlign = getTextAlignForAngle(angle); + adjustPointPositionForLabelHeight(angle, scale._pointLabelSizes[i], pointLabelPosition); + fillText(ctx, scale.pointLabels[i] || '', pointLabelPosition, plFont.size); + } + } + + function drawRadiusLine(scale, gridLineOpts, radius, index) { + var ctx = scale.ctx; + ctx.strokeStyle = helpers.getValueAtIndexOrDefault(gridLineOpts.color, index - 1); + ctx.lineWidth = helpers.getValueAtIndexOrDefault(gridLineOpts.lineWidth, index - 1); + + if (scale.options.lineArc) { + // Draw circular arcs between the points + ctx.beginPath(); + ctx.arc(scale.xCenter, scale.yCenter, radius, 0, Math.PI * 2); + ctx.closePath(); + ctx.stroke(); + } else { + // Draw straight lines connecting each index + var valueCount = getValueCount(scale); + + if (valueCount === 0) { + return; + } + + ctx.beginPath(); + var pointPosition = scale.getPointPosition(0, radius); + ctx.moveTo(pointPosition.x, pointPosition.y); + + for (var i = 1; i < valueCount; i++) { + pointPosition = scale.getPointPosition(i, radius); + ctx.lineTo(pointPosition.x, pointPosition.y); + } + + ctx.closePath(); + ctx.stroke(); + } + } + + function numberOrZero(param) { + return helpers.isNumber(param) ? param : 0; + } + var LinearRadialScale = Chart.LinearScaleBase.extend({ - getValueCount: function() { - return this.chart.data.labels.length; - }, setDimensions: function() { var me = this; var opts = me.options; @@ -68,9 +324,8 @@ module.exports = function(Chart) { determineDataLimits: function() { var me = this; var chart = me.chart; - me.min = null; - me.max = null; - + var min = Number.POSITIVE_INFINITY; + var max = Number.NEGATIVE_INFINITY; helpers.each(chart.data.datasets, function(dataset, datasetIndex) { if (chart.isDatasetVisible(datasetIndex)) { @@ -82,21 +337,15 @@ module.exports = function(Chart) { return; } - if (me.min === null) { - me.min = value; - } else if (value < me.min) { - me.min = value; - } - - if (me.max === null) { - me.max = value; - } else if (value > me.max) { - me.max = value; - } + min = Math.min(value, min); + max = Math.max(value, max); }); } }); + me.min = (min === Number.POSITIVE_INFINITY ? 0 : min); + me.max = (max === Number.NEGATIVE_INFINITY ? 0 : max); + // Common base implementation to handle ticks.min, ticks.max, ticks.beginAtZero me.handleTickRangeOptions(); }, @@ -116,122 +365,46 @@ module.exports = function(Chart) { return +this.getRightValue(this.chart.data.datasets[datasetIndex].data[index]); }, fit: function() { - /* - * Right, this is really confusing and there is a lot of maths going on here - * The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9 - * - * Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif - * - * Solution: - * - * We assume the radius of the polygon is half the size of the canvas at first - * at each index we check if the text overlaps. - * - * Where it does, we store that angle and that index. - * - * After finding the largest index and angle we calculate how much we need to remove - * from the shape radius to move the point inwards by that x. - * - * We average the left and right distances to get the maximum shape radius that can fit in the box - * along with labels. - * - * Once we have that, we can find the centre point for the chart, by taking the x text protrusion - * on each side, removing that from the size, halving it and adding the left x protrusion width. - * - * This will mean we have a shape fitted to the canvas, as large as it can be with the labels - * and position it in the most space efficient manner - * - * https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif - */ - - var pointLabels = this.options.pointLabels; - var pointLabelFontSize = helpers.getValueOrDefault(pointLabels.fontSize, globalDefaults.defaultFontSize); - var pointLabeFontStyle = helpers.getValueOrDefault(pointLabels.fontStyle, globalDefaults.defaultFontStyle); - var pointLabeFontFamily = helpers.getValueOrDefault(pointLabels.fontFamily, globalDefaults.defaultFontFamily); - var pointLabeFont = helpers.fontString(pointLabelFontSize, pointLabeFontStyle, pointLabeFontFamily); - - // Get maximum radius of the polygon. Either half the height (minus the text width) or half the width. - // Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points - var largestPossibleRadius = helpers.min([(this.height / 2 - pointLabelFontSize - 5), this.width / 2]), - pointPosition, - i, - textWidth, - halfTextWidth, - furthestRight = this.width, - furthestRightIndex, - furthestRightAngle, - furthestLeft = 0, - furthestLeftIndex, - furthestLeftAngle, - xProtrusionLeft, - xProtrusionRight, - radiusReductionRight, - radiusReductionLeft; - this.ctx.font = pointLabeFont; - - for (i = 0; i < this.getValueCount(); i++) { - // 5px to space the text slightly out - similar to what we do in the draw function. - pointPosition = this.getPointPosition(i, largestPossibleRadius); - textWidth = this.ctx.measureText(this.pointLabels[i] ? this.pointLabels[i] : '').width + 5; - - // Add quarter circle to make degree 0 mean top of circle - var angleRadians = this.getIndexAngle(i) + (Math.PI / 2); - var angle = (angleRadians * 360 / (2 * Math.PI)) % 360; - - if (angle === 0 || angle === 180) { - // At angle 0 and 180, we're at exactly the top/bottom - // of the radar chart, so text will be aligned centrally, so we'll half it and compare - // w/left and right text sizes - halfTextWidth = textWidth / 2; - if (pointPosition.x + halfTextWidth > furthestRight) { - furthestRight = pointPosition.x + halfTextWidth; - furthestRightIndex = i; - } - if (pointPosition.x - halfTextWidth < furthestLeft) { - furthestLeft = pointPosition.x - halfTextWidth; - furthestLeftIndex = i; - } - } else if (angle < 180) { - // Less than half the values means we'll left align the text - if (pointPosition.x + textWidth > furthestRight) { - furthestRight = pointPosition.x + textWidth; - furthestRightIndex = i; - } - // More than half the values means we'll right align the text - } else if (pointPosition.x - textWidth < furthestLeft) { - furthestLeft = pointPosition.x - textWidth; - furthestLeftIndex = i; - } + if (this.options.lineArc) { + fit(this); + } else { + fitWithPointLabels(this); } - - xProtrusionLeft = furthestLeft; - xProtrusionRight = Math.ceil(furthestRight - this.width); - - furthestRightAngle = this.getIndexAngle(furthestRightIndex); - furthestLeftAngle = this.getIndexAngle(furthestLeftIndex); - - radiusReductionRight = xProtrusionRight / Math.sin(furthestRightAngle + Math.PI / 2); - radiusReductionLeft = xProtrusionLeft / Math.sin(furthestLeftAngle + Math.PI / 2); - - // Ensure we actually need to reduce the size of the chart - radiusReductionRight = (helpers.isNumber(radiusReductionRight)) ? radiusReductionRight : 0; - radiusReductionLeft = (helpers.isNumber(radiusReductionLeft)) ? radiusReductionLeft : 0; - - this.drawingArea = Math.round(largestPossibleRadius - (radiusReductionLeft + radiusReductionRight) / 2); - this.setCenterPoint(radiusReductionLeft, radiusReductionRight); }, - setCenterPoint: function(leftMovement, rightMovement) { + /** + * Set radius reductions and determine new radius and center point + * @private + */ + setReductions: function(largestPossibleRadius, furthestLimits, furthestAngles) { + var me = this; + var radiusReductionLeft = furthestLimits.l / Math.sin(furthestAngles.l); + var radiusReductionRight = Math.max(furthestLimits.r - me.width, 0) / Math.sin(furthestAngles.r); + var radiusReductionTop = -furthestLimits.t / Math.cos(furthestAngles.t); + var radiusReductionBottom = -Math.max(furthestLimits.b - me.height, 0) / Math.cos(furthestAngles.b); + + radiusReductionLeft = numberOrZero(radiusReductionLeft); + radiusReductionRight = numberOrZero(radiusReductionRight); + radiusReductionTop = numberOrZero(radiusReductionTop); + radiusReductionBottom = numberOrZero(radiusReductionBottom); + + me.drawingArea = Math.min( + Math.round(largestPossibleRadius - (radiusReductionLeft + radiusReductionRight) / 2), + Math.round(largestPossibleRadius - (radiusReductionTop + radiusReductionBottom) / 2)); + me.setCenterPoint(radiusReductionLeft, radiusReductionRight, radiusReductionTop, radiusReductionBottom); + }, + setCenterPoint: function(leftMovement, rightMovement, topMovement, bottomMovement) { var me = this; var maxRight = me.width - rightMovement - me.drawingArea, - maxLeft = leftMovement + me.drawingArea; + maxLeft = leftMovement + me.drawingArea, + maxTop = topMovement + me.drawingArea, + maxBottom = me.height - bottomMovement - me.drawingArea; me.xCenter = Math.round(((maxLeft + maxRight) / 2) + me.left); - // Always vertically in the centre as the text height doesn't change - me.yCenter = Math.round((me.height / 2) + me.top); + me.yCenter = Math.round(((maxTop + maxBottom) / 2) + me.top); }, getIndexAngle: function(index) { - var angleMultiplier = (Math.PI * 2) / this.getValueCount(); + var angleMultiplier = (Math.PI * 2) / getValueCount(this); var startAngle = this.chart.options && this.chart.options.startAngle ? this.chart.options.startAngle : 0; @@ -239,7 +412,7 @@ module.exports = function(Chart) { var startAngleRadians = startAngle * Math.PI * 2 / 360; // Start from the top instead of right, so remove a quarter of the circle - return index * angleMultiplier - (Math.PI / 2) + startAngleRadians; + return index * angleMultiplier + startAngleRadians; }, getDistanceFromCenterForValue: function(value) { var me = this; @@ -257,7 +430,7 @@ module.exports = function(Chart) { }, getPointPosition: function(index, distanceFromCenter) { var me = this; - var thisAngle = me.getIndexAngle(index); + var thisAngle = me.getIndexAngle(index) - (Math.PI / 2); return { x: Math.round(Math.cos(thisAngle) * distanceFromCenter) + me.xCenter, y: Math.round(Math.sin(thisAngle) * distanceFromCenter) + me.yCenter @@ -284,8 +457,6 @@ module.exports = function(Chart) { var opts = me.options; var gridLineOpts = opts.gridLines; var tickOpts = opts.ticks; - var angleLineOpts = opts.angleLines; - var pointLabelOpts = opts.pointLabels; var getValueOrDefault = helpers.getValueOrDefault; if (opts.display) { @@ -305,29 +476,7 @@ module.exports = function(Chart) { // Draw circular lines around the scale if (gridLineOpts.display && index !== 0) { - ctx.strokeStyle = helpers.getValueAtIndexOrDefault(gridLineOpts.color, index - 1); - ctx.lineWidth = helpers.getValueAtIndexOrDefault(gridLineOpts.lineWidth, index - 1); - - if (opts.lineArc) { - // Draw circular arcs between the points - ctx.beginPath(); - ctx.arc(me.xCenter, me.yCenter, yCenterOffset, 0, Math.PI * 2); - ctx.closePath(); - ctx.stroke(); - } else { - // Draw straight lines connecting each index - ctx.beginPath(); - for (var i = 0; i < me.getValueCount(); i++) { - var pointPosition = me.getPointPosition(i, yCenterOffset); - if (i === 0) { - ctx.moveTo(pointPosition.x, pointPosition.y); - } else { - ctx.lineTo(pointPosition.x, pointPosition.y); - } - } - ctx.closePath(); - ctx.stroke(); - } + drawRadiusLine(me, gridLineOpts, yCenterOffset, index); } if (tickOpts.display) { @@ -354,59 +503,7 @@ module.exports = function(Chart) { }); if (!opts.lineArc) { - ctx.lineWidth = angleLineOpts.lineWidth; - ctx.strokeStyle = angleLineOpts.color; - - var outerDistance = me.getDistanceFromCenterForValue(opts.reverse ? me.min : me.max); - - // Point Label Font - var pointLabelFontSize = getValueOrDefault(pointLabelOpts.fontSize, globalDefaults.defaultFontSize); - var pointLabeFontStyle = getValueOrDefault(pointLabelOpts.fontStyle, globalDefaults.defaultFontStyle); - var pointLabeFontFamily = getValueOrDefault(pointLabelOpts.fontFamily, globalDefaults.defaultFontFamily); - var pointLabeFont = helpers.fontString(pointLabelFontSize, pointLabeFontStyle, pointLabeFontFamily); - - for (var i = me.getValueCount() - 1; i >= 0; i--) { - if (angleLineOpts.display) { - var outerPosition = me.getPointPosition(i, outerDistance); - ctx.beginPath(); - ctx.moveTo(me.xCenter, me.yCenter); - ctx.lineTo(outerPosition.x, outerPosition.y); - ctx.stroke(); - ctx.closePath(); - } - // Extra 3px out for some label spacing - var pointLabelPosition = me.getPointPosition(i, outerDistance + 5); - - // Keep this in loop since we may support array properties here - var pointLabelFontColor = getValueOrDefault(pointLabelOpts.fontColor, globalDefaults.defaultFontColor); - ctx.font = pointLabeFont; - ctx.fillStyle = pointLabelFontColor; - - var pointLabels = me.pointLabels; - - // Add quarter circle to make degree 0 mean top of circle - var angleRadians = this.getIndexAngle(i) + (Math.PI / 2); - var angle = (angleRadians * 360 / (2 * Math.PI)) % 360; - - if (angle === 0 || angle === 180) { - ctx.textAlign = 'center'; - } else if (angle < 180) { - ctx.textAlign = 'left'; - } else { - ctx.textAlign = 'right'; - } - - // Set the correct text baseline based on outer positioning - if (angle === 90 || angle === 270) { - ctx.textBaseline = 'middle'; - } else if (angle > 270 || angle < 90) { - ctx.textBaseline = 'bottom'; - } else { - ctx.textBaseline = 'top'; - } - - ctx.fillText(pointLabels[i] ? pointLabels[i] : '', pointLabelPosition.x, pointLabelPosition.y); - } + drawPointLabels(me); } } } diff --git a/test/controller.polarArea.tests.js b/test/controller.polarArea.tests.js index 3bd95eb231e..de7d4acfaac 100644 --- a/test/controller.polarArea.tests.js +++ b/test/controller.polarArea.tests.js @@ -96,9 +96,9 @@ describe('Polar area controller tests', function() { expect(meta.data.length).toBe(4); [ - {o: 156, s: -0.5 * Math.PI, e: 0}, - {o: 211, s: 0, e: 0.5 * Math.PI}, - {o: 45, s: 0.5 * Math.PI, e: Math.PI}, + {o: 168, s: -0.5 * Math.PI, e: 0}, + {o: 228, s: 0, e: 0.5 * Math.PI}, + {o: 48, s: 0.5 * Math.PI, e: Math.PI}, {o: 0, s: Math.PI, e: 1.5 * Math.PI} ].forEach(function(expected, i) { expect(meta.data[i]._model.x).toBeCloseToPixel(256); @@ -140,7 +140,7 @@ describe('Polar area controller tests', function() { expect(meta.data[0]._model.x).toBeCloseToPixel(256); expect(meta.data[0]._model.y).toBeCloseToPixel(272); expect(meta.data[0]._model.innerRadius).toBeCloseToPixel(0); - expect(meta.data[0]._model.outerRadius).toBeCloseToPixel(156); + expect(meta.data[0]._model.outerRadius).toBeCloseToPixel(168); expect(meta.data[0]._model).toEqual(jasmine.objectContaining({ startAngle: -0.5 * Math.PI, endAngle: 0, @@ -178,9 +178,9 @@ describe('Polar area controller tests', function() { expect(meta.data.length).toBe(4); [ - {o: 156, s: 0, e: 0.5 * Math.PI}, - {o: 211, s: 0.5 * Math.PI, e: Math.PI}, - {o: 45, s: Math.PI, e: 1.5 * Math.PI}, + {o: 168, s: 0, e: 0.5 * Math.PI}, + {o: 228, s: 0.5 * Math.PI, e: Math.PI}, + {o: 48, s: Math.PI, e: 1.5 * Math.PI}, {o: 0, s: 1.5 * Math.PI, e: 2.0 * Math.PI} ].forEach(function(expected, i) { expect(meta.data[i]._model.x).toBeCloseToPixel(256); diff --git a/test/scale.radialLinear.tests.js b/test/scale.radialLinear.tests.js index b4004f1bafe..69d6c167f92 100644 --- a/test/scale.radialLinear.tests.js +++ b/test/scale.radialLinear.tests.js @@ -342,9 +342,9 @@ describe('Test the radial linear scale', function() { } }); - expect(chart.scale.drawingArea).toBe(225); - expect(chart.scale.xCenter).toBe(256); - expect(chart.scale.yCenter).toBe(272); + expect(chart.scale.drawingArea).toBe(233); + expect(chart.scale.xCenter).toBe(247); + expect(chart.scale.yCenter).toBe(280); }); it('should correctly get the label for a given data index', function() { @@ -390,16 +390,16 @@ describe('Test the radial linear scale', function() { }); expect(chart.scale.getDistanceFromCenterForValue(chart.scale.min)).toBe(0); - expect(chart.scale.getDistanceFromCenterForValue(chart.scale.max)).toBe(225); + expect(chart.scale.getDistanceFromCenterForValue(chart.scale.max)).toBe(233); expect(chart.scale.getPointPositionForValue(1, 5)).toEqual({ - x: 269, - y: 268, + x: 261, + y: 275, }); chart.scale.options.reverse = true; chart.update(); - expect(chart.scale.getDistanceFromCenterForValue(chart.scale.min)).toBe(225); + expect(chart.scale.getDistanceFromCenterForValue(chart.scale.min)).toBe(233); expect(chart.scale.getDistanceFromCenterForValue(chart.scale.max)).toBe(0); }); @@ -431,14 +431,14 @@ describe('Test the radial linear scale', function() { var slice = 72; // (360 / 5) for (var i = 0; i < 5; i++) { - expect(radToNearestDegree(chart.scale.getIndexAngle(i))).toBe(15 + (slice * i) - 90); + expect(radToNearestDegree(chart.scale.getIndexAngle(i))).toBe(15 + (slice * i)); } chart.options.startAngle = 0; chart.update(); for (var x = 0; x < 5; x++) { - expect(radToNearestDegree(chart.scale.getIndexAngle(x))).toBe((slice * x) - 90); + expect(radToNearestDegree(chart.scale.getIndexAngle(x))).toBe((slice * x)); } }); }); From 2e5df0ff42c9ec542bd7c88de8f88f50a8ad5864 Mon Sep 17 00:00:00 2001 From: etimberg Date: Sat, 12 Nov 2016 22:38:25 -0500 Subject: [PATCH 18/61] Allow updating the config of a chart at runtime --- src/core/core.controller.js | 23 +++++++++++++ src/core/core.legend.js | 35 +++++++++++++++----- src/core/core.title.js | 43 +++++++++++++++--------- test/core.controller.tests.js | 49 +++++++++++++++++++++++++++ test/core.legend.tests.js | 62 +++++++++++++++++++++++++++++++++++ test/core.title.tests.js | 62 +++++++++++++++++++++++++++++++++++ 6 files changed, 251 insertions(+), 23 deletions(-) diff --git a/src/core/core.controller.js b/src/core/core.controller.js index bdc98ee04aa..ebbd081afa6 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -168,6 +168,26 @@ module.exports = function(Chart) { return config; } + /** + * Updates the config of the chart + * @param chart {Chart.Controller} chart to update the options for + */ + 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; + }); + } + + // Tooltip + chart.tooltip._options = newOptions.tooltips; + } + /** * @class Chart.Controller * The main controller of a chart. @@ -435,8 +455,11 @@ module.exports = function(Chart) { this.tooltip.initialize(); }, + update: function(animationDuration, lazy) { var me = this; + + updateConfig(me); Chart.plugins.notify('beforeUpdate', [me]); // In case the entire data object changed diff --git a/src/core/core.legend.js b/src/core/core.legend.js index 07349fd80b9..4ab51ba5dbf 100644 --- a/src/core/core.legend.js +++ b/src/core/core.legend.js @@ -489,20 +489,39 @@ module.exports = function(Chart) { } }); + function createNewLegendAndAttach(chartInstance, legendOpts) { + var legend = new Chart.Legend({ + ctx: chartInstance.chart.ctx, + options: legendOpts, + chart: chartInstance + }); + chartInstance.legend = legend; + Chart.layoutService.addBox(chartInstance, legend); + } + // Register the legend plugin Chart.plugins.register({ beforeInit: function(chartInstance) { - var opts = chartInstance.options; - var legendOpts = opts.legend; + var legendOpts = chartInstance.options.legend; if (legendOpts) { - chartInstance.legend = new Chart.Legend({ - ctx: chartInstance.chart.ctx, - options: legendOpts, - chart: chartInstance - }); + createNewLegendAndAttach(chartInstance, legendOpts); + } + }, + beforeUpdate: function(chartInstance) { + var legendOpts = chartInstance.options.legend; - Chart.layoutService.addBox(chartInstance, chartInstance.legend); + if (legendOpts) { + legendOpts = helpers.configMerge(Chart.defaults.global.legend, legendOpts); + + if (chartInstance.legend) { + chartInstance.legend.options = legendOpts; + } else { + createNewLegendAndAttach(chartInstance, legendOpts); + } + } else { + Chart.layoutService.removeBox(chartInstance, chartInstance.legend); + delete chartInstance.legend; } } }); diff --git a/src/core/core.title.js b/src/core/core.title.js index a7663258a13..5b2d989f8a7 100644 --- a/src/core/core.title.js +++ b/src/core/core.title.js @@ -22,7 +22,6 @@ module.exports = function(Chart) { initialize: function(config) { var me = this; helpers.extend(me, config); - me.options = helpers.configMerge(Chart.defaults.global.title, config.options); // Contains hit boxes for each dataset (in dataset order) me.legendHitBoxes = []; @@ -30,12 +29,7 @@ module.exports = function(Chart) { // These methods are ordered by lifecycle. Utilities then follow. - beforeUpdate: function() { - var chartOpts = this.chart.options; - if (chartOpts && chartOpts.title) { - this.options = helpers.configMerge(Chart.defaults.global.title, chartOpts.title); - } - }, + beforeUpdate: noop, update: function(maxWidth, maxHeight, margins) { var me = this; @@ -187,20 +181,39 @@ module.exports = function(Chart) { } }); + function createNewTitleBlockAndAttach(chartInstance, titleOpts) { + var title = new Chart.Title({ + ctx: chartInstance.chart.ctx, + options: titleOpts, + chart: chartInstance + }); + chartInstance.titleBlock = title; + Chart.layoutService.addBox(chartInstance, title); + } + // Register the title plugin Chart.plugins.register({ beforeInit: function(chartInstance) { - var opts = chartInstance.options; - var titleOpts = opts.title; + var titleOpts = chartInstance.options.title; + + if (titleOpts) { + createNewTitleBlockAndAttach(chartInstance, titleOpts); + } + }, + beforeUpdate: function(chartInstance) { + var titleOpts = chartInstance.options.title; if (titleOpts) { - chartInstance.titleBlock = new Chart.Title({ - ctx: chartInstance.chart.ctx, - options: titleOpts, - chart: chartInstance - }); + titleOpts = helpers.configMerge(Chart.defaults.global.title, titleOpts); - Chart.layoutService.addBox(chartInstance, chartInstance.titleBlock); + if (chartInstance.titleBlock) { + chartInstance.titleBlock.options = titleOpts; + } else { + createNewTitleBlockAndAttach(chartInstance, titleOpts); + } + } else { + Chart.layoutService.removeBox(chartInstance, chartInstance.titleBlock); + delete chartInstance.titleBlock; } } }); diff --git a/test/core.controller.tests.js b/test/core.controller.tests.js index cc99b0d7fb1..44e7d3d21ed 100644 --- a/test/core.controller.tests.js +++ b/test/core.controller.tests.js @@ -830,4 +830,53 @@ describe('Chart.Controller', function() { expect(meta.data[3]._model.y).toBe(484); }); }); + + describe('config update', function() { + it ('should update scales options', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + responsive: true + } + }); + + chart.options.scales.yAxes[0].ticks.min = 0; + chart.options.scales.yAxes[0].ticks.max = 10; + chart.update(); + + var yScale = chart.scales['y-axis-0']; + expect(yScale.options.ticks.min).toBe(0); + expect(yScale.options.ticks.max).toBe(10); + }); + + it ('should update tooltip options', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + responsive: true + } + }); + + var newTooltipConfig = { + mode: 'dataset', + intersect: false + }; + chart.options.tooltips = newTooltipConfig; + + chart.update(); + expect(chart.tooltip._options).toEqual(jasmine.objectContaining(newTooltipConfig)); + }); + }); }); diff --git a/test/core.legend.tests.js b/test/core.legend.tests.js index 3cdeddc903c..109a209e05c 100644 --- a/test/core.legend.tests.js +++ b/test/core.legend.tests.js @@ -367,4 +367,66 @@ describe('Legend block tests', function() { "args": ["dataset3", 228, 132] }]);*/ }); + + describe('config update', function() { + it ('should update the options', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + legend: { + display: true + } + } + }); + expect(chart.legend.options.display).toBe(true); + + chart.options.legend.display = false; + chart.update(); + expect(chart.legend.options.display).toBe(false); + }); + + it ('should remove the legend if the new options are false', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + } + }); + expect(chart.legend).not.toBe(undefined); + + chart.options.legend = false; + chart.update(); + expect(chart.legend).toBe(undefined); + }); + + it ('should create the legend if the legend options are changed to exist', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + legend: false + } + }); + expect(chart.legend).toBe(undefined); + + chart.options.legend = {}; + chart.update(); + expect(chart.legend).not.toBe(undefined); + expect(chart.legend.options).toEqual(jasmine.objectContaining(Chart.defaults.global.legend)); + }); + }); }); diff --git a/test/core.title.tests.js b/test/core.title.tests.js index 7046052781e..f334a1c6c67 100644 --- a/test/core.title.tests.js +++ b/test/core.title.tests.js @@ -207,4 +207,66 @@ describe('Title block tests', function() { args: [] }]); }); + + describe('config update', function() { + it ('should update the options', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + title: { + display: true + } + } + }); + expect(chart.titleBlock.options.display).toBe(true); + + chart.options.title.display = false; + chart.update(); + expect(chart.titleBlock.options.display).toBe(false); + }); + + it ('should remove the title if the new options are false', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + } + }); + expect(chart.titleBlock).not.toBe(undefined); + + chart.options.title = false; + chart.update(); + expect(chart.titleBlock).toBe(undefined); + }); + + it ('should create the title if the title options are changed to exist', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + title: false + } + }); + expect(chart.titleBlock).toBe(undefined); + + chart.options.title = {}; + chart.update(); + expect(chart.titleBlock).not.toBe(undefined); + expect(chart.titleBlock.options).toEqual(jasmine.objectContaining(Chart.defaults.global.title)); + }); + }); }); From 97f6c8f12d75981ace1df5662f544f0758653170 Mon Sep 17 00:00:00 2001 From: Christopher Moeller Date: Tue, 25 Oct 2016 16:32:27 -0500 Subject: [PATCH 19/61] Add rectRounded point style --- docs/03-Line-Chart.md | 2 +- docs/05-Radar-Chart.md | 2 +- src/core/core.canvasHelpers.js | 8 ++++++++ test/element.point.tests.js | 24 ++++++++++++++++++++++++ 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/docs/03-Line-Chart.md b/docs/03-Line-Chart.md index f62a20879df..24b77cfe6df 100644 --- a/docs/03-Line-Chart.md +++ b/docs/03-Line-Chart.md @@ -57,7 +57,7 @@ pointHitRadius | `Number or Array` | The pixel size of the non-displayed pointHoverBackgroundColor | `Color or Array` | Point background color when hovered pointHoverBorderColor | `Color or Array` | Point border color when hovered pointHoverBorderWidth | `Number or Array` | Border width of point when hovered -pointStyle | `String, Array, Image, Array` | The style of point. Options are 'circle', 'triangle', 'rect', 'rectRot', 'cross', 'crossRot', 'star', 'line', and 'dash'. If the option is an image, that image is drawn on the canvas using `drawImage`. +pointStyle | `String, Array, Image, Array` | The style of point. Options are 'circle', 'triangle', 'rect', 'rectRounded', 'rectRot', 'cross', 'crossRot', 'star', 'line', and 'dash'. If the option is an image, that image is drawn on the canvas using `drawImage`. 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 steppedLine | `Boolean` | If true, the line is shown as a stepped line and 'lineTension' will be ignored diff --git a/docs/05-Radar-Chart.md b/docs/05-Radar-Chart.md index 8fc560b3f04..977574faf6c 100644 --- a/docs/05-Radar-Chart.md +++ b/docs/05-Radar-Chart.md @@ -50,7 +50,7 @@ pointHitRadius | `Number or Array` | The pixel size of the non-displayed pointHoverBackgroundColor | `Color or Array` | Point background color when hovered pointHoverBorderColor | `Color or Array` | Point border color when hovered pointHoverBorderWidth | `Number or Array` | Border width of point when hovered -pointStyle | `String or Array` | The style of point. Options include 'circle', 'triangle', 'rect', 'rectRot', 'cross', 'crossRot', 'star', 'line', and 'dash' +pointStyle | `String or Array` | The style of point. Options include 'circle', 'triangle', 'rect', 'rectRounded', 'rectRot', 'cross', 'crossRot', 'star', 'line', and 'dash' An example data object using these attributes is shown below. diff --git a/src/core/core.canvasHelpers.js b/src/core/core.canvasHelpers.js index 439df7dba1b..42fac5c00b5 100644 --- a/src/core/core.canvasHelpers.js +++ b/src/core/core.canvasHelpers.js @@ -43,6 +43,14 @@ module.exports = function(Chart) { ctx.fillRect(x - size, y - size, 2 * size, 2 * size); ctx.strokeRect(x - size, y - size, 2 * size, 2 * size); break; + case 'rectRounded': + var offset = radius / Math.SQRT2; + var leftX = x - offset; + var topY = y - offset; + var sideSize = Math.SQRT2 * radius; + Chart.helpers.drawRoundedRectangle(ctx, leftX, topY, sideSize, sideSize, radius / 2); + ctx.fill(); + break; case 'rectRot': size = 1 / Math.SQRT2 * radius; ctx.beginPath(); diff --git a/test/element.point.tests.js b/test/element.point.tests.js index 61fe3d09a4e..01eba3046ef 100644 --- a/test/element.point.tests.js +++ b/test/element.point.tests.js @@ -208,6 +208,30 @@ describe('Point element tests', function() { args: [] }]); + var drawRoundedRectangleSpy = jasmine.createSpy('drawRoundedRectangle'); + var drawRoundedRectangle = Chart.helpers.drawRoundedRectangle; + var offset = point._view.radius / Math.SQRT2; + Chart.helpers.drawRoundedRectangle = drawRoundedRectangleSpy; + mockContext.resetCalls(); + point._view.pointStyle = 'rectRounded'; + point.draw(); + + expect(drawRoundedRectangleSpy).toHaveBeenCalledWith( + mockContext, + 10 - offset, + 15 - offset, + Math.SQRT2 * 2, + Math.SQRT2 * 2, + 2 / 2 + ); + expect(mockContext.getCalls()).toContain( + jasmine.objectContaining({ + name: 'fill', + args: [], + }) + ); + + Chart.helpers.drawRoundedRectangle = drawRoundedRectangle; mockContext.resetCalls(); point._view.pointStyle = 'rectRot'; point.draw(); From 09f30be9eb753614779a33104b1bca61a9a29a61 Mon Sep 17 00:00:00 2001 From: Christopher Moeller Date: Tue, 25 Oct 2016 18:12:41 -0500 Subject: [PATCH 20/61] Add rectRounded to point style sample --- samples/line/point-styles.html | 1 + 1 file changed, 1 insertion(+) diff --git a/samples/line/point-styles.html b/samples/line/point-styles.html index 547b1ea0a2b..0056e749976 100644 --- a/samples/line/point-styles.html +++ b/samples/line/point-styles.html @@ -70,6 +70,7 @@ 'circle', 'triangle', 'rect', + 'rectRounded', 'rectRot', 'cross', 'crossRot', From 7a8f20e88fb2029e82c9903be99412878368405a Mon Sep 17 00:00:00 2001 From: etimberg Date: Sat, 26 Nov 2016 12:29:15 -0500 Subject: [PATCH 21/61] Fix monotone cubic interpolation when two adjacent points are at the exact same x pixel value --- src/core/core.helpers.js | 5 ++++- test/core.helpers.tests.js | 16 ++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index 542088f610b..506ad900397 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -361,7 +361,10 @@ module.exports = function(Chart) { pointBefore = i > 0 ? pointsWithTangents[i - 1] : null; pointAfter = i < pointsLen - 1 ? pointsWithTangents[i + 1] : null; if (pointAfter && !pointAfter.model.skip) { - pointCurrent.deltaK = (pointAfter.model.y - pointCurrent.model.y) / (pointAfter.model.x - pointCurrent.model.x); + var slopeDeltaX = (pointAfter.model.x - pointCurrent.model.x); + + // In the case of two points that appear at the same x pixel, slopeDeltaX is 0 + pointCurrent.deltaK = slopeDeltaX !== 0 ? (pointAfter.model.y - pointCurrent.model.y) / slopeDeltaX : 0; } if (!pointBefore || pointBefore.model.skip) { diff --git a/test/core.helpers.tests.js b/test/core.helpers.tests.js index 4cd200ae3b8..75e0baf900e 100644 --- a/test/core.helpers.tests.js +++ b/test/core.helpers.tests.js @@ -412,6 +412,7 @@ describe('Core helper tests', function() { {_model: {x: 27, y: 125, skip: false}}, {_model: {x: 30, y: 105, skip: false}}, {_model: {x: 33, y: 110, skip: false}}, + {_model: {x: 33, y: 110, skip: false}}, {_model: {x: 36, y: 170, skip: false}} ]; helpers.splineCurveMonotone(dataPoints); @@ -532,9 +533,20 @@ describe('Core helper tests', function() { y: 110, skip: false, controlPointPreviousX: 32, - controlPointPreviousY: 105, + controlPointPreviousY: 110, + controlPointNextX: 33, + controlPointNextY: 110 + } + }, + { + _model: { + x: 33, + y: 110, + skip: false, + controlPointPreviousX: 33, + controlPointPreviousY: 110, controlPointNextX: 34, - controlPointNextY: 115 + controlPointNextY: 110 } }, { From d39ac38ce1b9c608751d66f70317d3fc943303b1 Mon Sep 17 00:00:00 2001 From: SAiTO TOSHiKi Date: Tue, 29 Nov 2016 07:28:39 +0800 Subject: [PATCH 22/61] Fix : Tooltip label for category scale. (#3649) --- src/scales/scale.category.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scales/scale.category.js b/src/scales/scale.category.js index 8a65cb2e7a0..3cee4ebf71f 100644 --- a/src/scales/scale.category.js +++ b/src/scales/scale.category.js @@ -54,10 +54,10 @@ module.exports = function(Chart) { var data = me.chart.data; var isHorizontal = me.isHorizontal(); - if ((data.xLabels && isHorizontal) || (data.yLabels && !isHorizontal)) { + if (data.yLabels && !isHorizontal) { return me.getRightValue(data.datasets[datasetIndex].data[index]); } - return me.ticks[index]; + return me.ticks[index - me.minIndex]; }, // Used to get data value locations. Value can either be an index or a numerical value From b6807b2dd9108c1bd9bdc1b0a68c025a872d7b96 Mon Sep 17 00:00:00 2001 From: Jerry Chang Date: Mon, 14 Nov 2016 20:30:18 -0800 Subject: [PATCH 23/61] fixed tooltip labelling on Bar Chart when min is defined added helper method to adjust the index pass in chartConfig rather than access within method, make it easier to test added semi-colon at the end of helper method added test for adjustIndex helper method fixed lint issues added integration test for the interaction of trigger an event over the bar . . moved adjustIndex into element helper removed method from helper and adjusted method in core.interaction added test for the element adjustIndex helper added a skipIndexAdjustment method to handle when to skip the adjustment along with test cases fixed lint issues removed the test for the helper method --- src/core/core.element.js | 18 ++++++++ src/core/core.interaction.js | 1 + test/core.element.tests.js | 84 ++++++++++++++++++++++++++++++++++ test/core.interaction.tests.js | 51 +++++++++++++++++++++ 4 files changed, 154 insertions(+) diff --git a/src/core/core.element.js b/src/core/core.element.js index d8dc57208a4..778f4119443 100644 --- a/src/core/core.element.js +++ b/src/core/core.element.js @@ -88,6 +88,24 @@ module.exports = function(Chart) { hasValue: function() { return helpers.isNumber(this._model.x) && helpers.isNumber(this._model.y); + }, + + + skipIndexAdjustment: function(config) { + var moreThanOneAxes = config.options.scales.xAxes.length > 1; + var min = config.options.scales.xAxes[0].ticks.min; + return this._adjustedIndex || min === undefined || moreThanOneAxes; + }, + + adjustIndex: function(config) { + var min = config.options.scales.xAxes[0].ticks.min; + + if (this.skipIndexAdjustment(config)) { + return; + } + + this._index -= config.data.labels.indexOf(min); + this._adjustedIndex = true; } }); diff --git a/src/core/core.interaction.js b/src/core/core.interaction.js index aacdda19c38..e3bd4bac6be 100644 --- a/src/core/core.interaction.js +++ b/src/core/core.interaction.js @@ -21,6 +21,7 @@ module.exports = function(Chart) { for (j = 0, jlen = meta.data.length; j < jlen; ++j) { var element = meta.data[j]; if (!element._view.skip) { + element.adjustIndex(chart.config); handler(element); } } diff --git a/test/core.element.tests.js b/test/core.element.tests.js index 7d194562e73..d18685dca3a 100644 --- a/test/core.element.tests.js +++ b/test/core.element.tests.js @@ -42,4 +42,88 @@ describe('Core element tests', function() { colorProp: 'rgb(64, 64, 0)', }); }); + + it ('should adjust the index of the element passed in', function() { + var chartConfig = { + options: { + scales: { + xAxes: [{ + ticks: { + min: 'Point 2' + } + }] + } + }, + data: { + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }; + + var element = new Chart.Element({ + _index: 1 + }); + + element.adjustIndex(chartConfig); + + expect(element._adjustedIndex).toEqual(true); + expect(element._index).toEqual(0); + }); + + describe ('skipIndexAdjustment method', function() { + var element; + + beforeEach(function() { + element = new Chart.Element({}); + }); + + it ('should return true when min is undefined', function() { + var chartConfig = { + options: { + scales: { + xAxes: [{ + ticks: { + min: undefined + } + }] + } + } + }; + expect(element.skipIndexAdjustment(chartConfig)).toEqual(true); + }); + + it ('should return true when index is already adjusted (_adjustedIndex = true)', function() { + var chartConfig = { + options: { + scales: { + xAxes: [{ + ticks: { + min: 'Point 1' + } + }] + } + } + }; + element._adjustedIndex = true; + expect(element.skipIndexAdjustment(chartConfig)).toEqual(true); + }); + + it ('should return true when more than one xAxes is defined', function() { + var chartConfig = { + options: { + scales: { + xAxes: [{ + ticks: { + min: 'Point 1' + } + }, { + ticks: { + min: 'Point 2' + } + }] + } + } + }; + expect(element.skipIndexAdjustment(chartConfig)).toEqual(true); + }); + }); }); diff --git a/test/core.interaction.tests.js b/test/core.interaction.tests.js index 7d8e339caf9..9d704ad342d 100644 --- a/test/core.interaction.tests.js +++ b/test/core.interaction.tests.js @@ -43,6 +43,57 @@ describe('Core.Interaction', function() { expect(elements).toEqual([point, meta1.data[1]]); }); + it ('should start at index 0 within sliced dataset when min is defined', function() { + var chartInstance = window.acquireChart({ + type: 'line', + options: { + scales: { + xAxes: [{ + ticks: { + min: 'March', + max: 'May' + }, + categoryPercentage: 1, + barPercentage: 1, + }] + } + }, + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 30, 39, 20, 25, 34, 1], + }, { + label: 'Dataset 2', + data: [10, 30, 39, 20, 25, 34, 1], + }], + labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], + } + }); + + // Trigger an event over top of the + var meta0 = chartInstance.getDatasetMeta(0); + var point = meta0.data[2]; + + var node = chartInstance.chart.canvas; + var rect = node.getBoundingClientRect(); + + var evt = { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.left + point._model.x, + clientY: rect.top + point._model.y, + currentTarget: node + }; + + var elements = Chart.Interaction.modes.point(chartInstance, evt); + + elements.forEach(function(element) { + expect(element._index).toEqual(0); + expect(element._adjustedIndex).toBeTruthy(); + }); + }); + it ('should return an empty array when no items are found', function() { var chartInstance = window.acquireChart({ type: 'line', From 5bf203037c667b5450093a1cd8164cef1461698e Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Tue, 29 Nov 2016 04:43:52 -0500 Subject: [PATCH 24/61] Do not notify plugins when a silent resize occurs (#3650) Prevent the resize method from notifying plugins if it is a silent resize. A silent resize occurs during startup and we do not want plugins to do anything here because the chart is not set up. --- src/core/core.controller.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/core/core.controller.js b/src/core/core.controller.js index ebbd081afa6..1abc81075da 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -312,16 +312,16 @@ module.exports = function(Chart) { helpers.retinaScale(chart); - // Notify any plugins about the resize - var newSize = {width: newWidth, height: newHeight}; - Chart.plugins.notify('resize', [me, newSize]); + if (!silent) { + // Notify any plugins about the resize + var newSize = {width: newWidth, height: newHeight}; + Chart.plugins.notify('resize', [me, newSize]); - // Notify of resize - if (me.options.onResize) { - me.options.onResize(me, newSize); - } + // Notify of resize + if (me.options.onResize) { + me.options.onResize(me, newSize); + } - if (!silent) { me.stop(); me.update(me.options.responsiveAnimationDuration); } From 3ff58e5065197aeb9bebaa668222952835da9c73 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Tue, 29 Nov 2016 08:22:47 -0500 Subject: [PATCH 25/61] Revert "Fixed tooltip labelling on Bar Chart when min is defined (#3618)" --- src/core/core.element.js | 18 -------- src/core/core.interaction.js | 1 - test/core.element.tests.js | 84 ---------------------------------- test/core.interaction.tests.js | 51 --------------------- 4 files changed, 154 deletions(-) diff --git a/src/core/core.element.js b/src/core/core.element.js index 778f4119443..d8dc57208a4 100644 --- a/src/core/core.element.js +++ b/src/core/core.element.js @@ -88,24 +88,6 @@ module.exports = function(Chart) { hasValue: function() { return helpers.isNumber(this._model.x) && helpers.isNumber(this._model.y); - }, - - - skipIndexAdjustment: function(config) { - var moreThanOneAxes = config.options.scales.xAxes.length > 1; - var min = config.options.scales.xAxes[0].ticks.min; - return this._adjustedIndex || min === undefined || moreThanOneAxes; - }, - - adjustIndex: function(config) { - var min = config.options.scales.xAxes[0].ticks.min; - - if (this.skipIndexAdjustment(config)) { - return; - } - - this._index -= config.data.labels.indexOf(min); - this._adjustedIndex = true; } }); diff --git a/src/core/core.interaction.js b/src/core/core.interaction.js index e3bd4bac6be..aacdda19c38 100644 --- a/src/core/core.interaction.js +++ b/src/core/core.interaction.js @@ -21,7 +21,6 @@ module.exports = function(Chart) { for (j = 0, jlen = meta.data.length; j < jlen; ++j) { var element = meta.data[j]; if (!element._view.skip) { - element.adjustIndex(chart.config); handler(element); } } diff --git a/test/core.element.tests.js b/test/core.element.tests.js index d18685dca3a..7d194562e73 100644 --- a/test/core.element.tests.js +++ b/test/core.element.tests.js @@ -42,88 +42,4 @@ describe('Core element tests', function() { colorProp: 'rgb(64, 64, 0)', }); }); - - it ('should adjust the index of the element passed in', function() { - var chartConfig = { - options: { - scales: { - xAxes: [{ - ticks: { - min: 'Point 2' - } - }] - } - }, - data: { - labels: ['Point 1', 'Point 2', 'Point 3'] - } - }; - - var element = new Chart.Element({ - _index: 1 - }); - - element.adjustIndex(chartConfig); - - expect(element._adjustedIndex).toEqual(true); - expect(element._index).toEqual(0); - }); - - describe ('skipIndexAdjustment method', function() { - var element; - - beforeEach(function() { - element = new Chart.Element({}); - }); - - it ('should return true when min is undefined', function() { - var chartConfig = { - options: { - scales: { - xAxes: [{ - ticks: { - min: undefined - } - }] - } - } - }; - expect(element.skipIndexAdjustment(chartConfig)).toEqual(true); - }); - - it ('should return true when index is already adjusted (_adjustedIndex = true)', function() { - var chartConfig = { - options: { - scales: { - xAxes: [{ - ticks: { - min: 'Point 1' - } - }] - } - } - }; - element._adjustedIndex = true; - expect(element.skipIndexAdjustment(chartConfig)).toEqual(true); - }); - - it ('should return true when more than one xAxes is defined', function() { - var chartConfig = { - options: { - scales: { - xAxes: [{ - ticks: { - min: 'Point 1' - } - }, { - ticks: { - min: 'Point 2' - } - }] - } - } - }; - expect(element.skipIndexAdjustment(chartConfig)).toEqual(true); - }); - }); }); diff --git a/test/core.interaction.tests.js b/test/core.interaction.tests.js index 9d704ad342d..7d8e339caf9 100644 --- a/test/core.interaction.tests.js +++ b/test/core.interaction.tests.js @@ -43,57 +43,6 @@ describe('Core.Interaction', function() { expect(elements).toEqual([point, meta1.data[1]]); }); - it ('should start at index 0 within sliced dataset when min is defined', function() { - var chartInstance = window.acquireChart({ - type: 'line', - options: { - scales: { - xAxes: [{ - ticks: { - min: 'March', - max: 'May' - }, - categoryPercentage: 1, - barPercentage: 1, - }] - } - }, - data: { - datasets: [{ - label: 'Dataset 1', - data: [10, 30, 39, 20, 25, 34, 1], - }, { - label: 'Dataset 2', - data: [10, 30, 39, 20, 25, 34, 1], - }], - labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], - } - }); - - // Trigger an event over top of the - var meta0 = chartInstance.getDatasetMeta(0); - var point = meta0.data[2]; - - var node = chartInstance.chart.canvas; - var rect = node.getBoundingClientRect(); - - var evt = { - view: window, - bubbles: true, - cancelable: true, - clientX: rect.left + point._model.x, - clientY: rect.top + point._model.y, - currentTarget: node - }; - - var elements = Chart.Interaction.modes.point(chartInstance, evt); - - elements.forEach(function(element) { - expect(element._index).toEqual(0); - expect(element._adjustedIndex).toBeTruthy(); - }); - }); - it ('should return an empty array when no items are found', function() { var chartInstance = window.acquireChart({ type: 'line', From b39c0e1f93518f2dcb1d1cc49ff04cff36d34a46 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Mon, 7 Nov 2016 21:17:20 -0500 Subject: [PATCH 26/61] Fix rotated label meaasurements (#2879, #3354). When measuring the first width and last width, the fact that arrays of text are present must be considered. In addition to fixing this, I did some general code cleanup in the fit and calculateLabelRotation methods. --- src/core/core.scale.js | 208 ++++++++++++++++++++--------------------- 1 file changed, 99 insertions(+), 109 deletions(-) diff --git a/src/core/core.scale.js b/src/core/core.scale.js index bc09cb0ae7b..1cc434f638d 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -50,6 +50,27 @@ module.exports = function(Chart) { } }; + function computeTextSize(context, tick, font) { + return helpers.isArray(tick) ? + helpers.longestText(context, font, tick) : + context.measureText(tick).width; + } + + function parseFontOptions(options) { + var getValueOrDefault = helpers.getValueOrDefault; + var globalDefaults = Chart.defaults.global; + var size = getValueOrDefault(options.fontSize, globalDefaults.defaultFontSize); + var style = getValueOrDefault(options.fontStyle, globalDefaults.defaultFontStyle); + var family = getValueOrDefault(options.fontFamily, globalDefaults.defaultFontFamily); + + return { + size: size, + style: style, + family: family, + font: helpers.fontString(size, style, family) + }; + } + Chart.Scale = Chart.Element.extend({ /** * Get the padding needed for the scale @@ -89,6 +110,7 @@ module.exports = function(Chart) { top: 0, bottom: 0 }, margins); + me.longestTextCache = me.longestTextCache || {}; // Dimensions me.beforeSetDimensions(); @@ -197,72 +219,42 @@ module.exports = function(Chart) { calculateTickRotation: function() { var me = this; var context = me.ctx; - var globalDefaults = Chart.defaults.global; - var optionTicks = me.options.ticks; + var tickOpts = me.options.ticks; // Get the width of each grid by calculating the difference // between x offsets between 0 and 1. - var tickFontSize = helpers.getValueOrDefault(optionTicks.fontSize, globalDefaults.defaultFontSize); - var tickFontStyle = helpers.getValueOrDefault(optionTicks.fontStyle, globalDefaults.defaultFontStyle); - var tickFontFamily = helpers.getValueOrDefault(optionTicks.fontFamily, globalDefaults.defaultFontFamily); - var tickLabelFont = helpers.fontString(tickFontSize, tickFontStyle, tickFontFamily); - context.font = tickLabelFont; - - var firstWidth = context.measureText(me.ticks[0]).width; - var lastWidth = context.measureText(me.ticks[me.ticks.length - 1]).width; - var firstRotated; - - me.labelRotation = optionTicks.minRotation || 0; - me.paddingRight = 0; - me.paddingLeft = 0; - - if (me.options.display) { - if (me.isHorizontal()) { - me.paddingRight = lastWidth / 2 + 3; - me.paddingLeft = firstWidth / 2 + 3; - - if (!me.longestTextCache) { - me.longestTextCache = {}; + var tickFont = parseFontOptions(tickOpts); + context.font = tickFont.font; + + var labelRotation = tickOpts.minRotation || 0; + + if (me.options.display && me.isHorizontal()) { + var originalLabelWidth = helpers.longestText(context, tickFont.font, me.ticks, me.longestTextCache); + var labelWidth = originalLabelWidth; + var cosRotation; + var sinRotation; + + // Allow 3 pixels x2 padding either side for label readability + var tickWidth = me.getPixelForTick(1) - me.getPixelForTick(0) - 6; + + // Max label rotation can be set or default to 90 - also act as a loop counter + while (labelWidth > tickWidth && labelRotation < tickOpts.maxRotation) { + var angleRadians = helpers.toRadians(labelRotation); + cosRotation = Math.cos(angleRadians); + sinRotation = Math.sin(angleRadians); + + if (sinRotation * originalLabelWidth > me.maxHeight) { + // go back one step + labelRotation--; + break; } - var originalLabelWidth = helpers.longestText(context, tickLabelFont, me.ticks, me.longestTextCache); - var labelWidth = originalLabelWidth; - var cosRotation; - var sinRotation; - - // Allow 3 pixels x2 padding either side for label readability - // only the index matters for a dataset scale, but we want a consistent interface between scales - var tickWidth = me.getPixelForTick(1) - me.getPixelForTick(0) - 6; - - // Max label rotation can be set or default to 90 - also act as a loop counter - while (labelWidth > tickWidth && me.labelRotation < optionTicks.maxRotation) { - cosRotation = Math.cos(helpers.toRadians(me.labelRotation)); - sinRotation = Math.sin(helpers.toRadians(me.labelRotation)); - - firstRotated = cosRotation * firstWidth; - - // We're right aligning the text now. - if (firstRotated + tickFontSize / 2 > me.yLabelWidth) { - me.paddingLeft = firstRotated + tickFontSize / 2; - } - - me.paddingRight = tickFontSize / 2; - - if (sinRotation * originalLabelWidth > me.maxHeight) { - // go back one step - me.labelRotation--; - break; - } - me.labelRotation++; - labelWidth = cosRotation * originalLabelWidth; - } + labelRotation++; + labelWidth = cosRotation * originalLabelWidth; } } - if (me.margins) { - me.paddingLeft = Math.max(me.paddingLeft - me.margins.left, 0); - me.paddingRight = Math.max(me.paddingRight - me.margins.right, 0); - } + me.labelRotation = labelRotation; }, afterCalculateTickRotation: function() { helpers.callCallback(this.options.afterCalculateTickRotation, [this]); @@ -282,20 +274,14 @@ module.exports = function(Chart) { }; var opts = me.options; - var globalDefaults = Chart.defaults.global; var tickOpts = opts.ticks; var scaleLabelOpts = opts.scaleLabel; var gridLineOpts = opts.gridLines; var display = opts.display; var isHorizontal = me.isHorizontal(); - var tickFontSize = helpers.getValueOrDefault(tickOpts.fontSize, globalDefaults.defaultFontSize); - var tickFontStyle = helpers.getValueOrDefault(tickOpts.fontStyle, globalDefaults.defaultFontStyle); - var tickFontFamily = helpers.getValueOrDefault(tickOpts.fontFamily, globalDefaults.defaultFontFamily); - var tickLabelFont = helpers.fontString(tickFontSize, tickFontStyle, tickFontFamily); - - var scaleLabelFontSize = helpers.getValueOrDefault(scaleLabelOpts.fontSize, globalDefaults.defaultFontSize); - + var tickFont = parseFontOptions(tickOpts); + var scaleLabelFontSize = parseFontOptions(scaleLabelOpts).size * 1.5; var tickMarkLength = opts.gridLines.tickMarkLength; // Width @@ -316,70 +302,79 @@ module.exports = function(Chart) { // Are we showing a title for the scale? if (scaleLabelOpts.display && display) { if (isHorizontal) { - minSize.height += (scaleLabelFontSize * 1.5); + minSize.height += scaleLabelFontSize; } else { - minSize.width += (scaleLabelFontSize * 1.5); + minSize.width += scaleLabelFontSize; } } + // Don't bother fitting the ticks if we are not showing them if (tickOpts.display && display) { - // Don't bother fitting the ticks if we are not showing them - if (!me.longestTextCache) { - me.longestTextCache = {}; - } - - var largestTextWidth = helpers.longestText(me.ctx, tickLabelFont, me.ticks, me.longestTextCache); + var largestTextWidth = helpers.longestText(me.ctx, tickFont.font, me.ticks, me.longestTextCache); var tallestLabelHeightInLines = helpers.numberOfLabelLines(me.ticks); - var lineSpace = tickFontSize * 0.5; + var lineSpace = tickFont.size * 0.5; if (isHorizontal) { // A horizontal axis is more constrained by the height. me.longestLabelWidth = largestTextWidth; + var angleRadians = helpers.toRadians(me.labelRotation); + var cosRotation = Math.cos(angleRadians); + var sinRotation = Math.sin(angleRadians); + // TODO - improve this calculation - var labelHeight = (Math.sin(helpers.toRadians(me.labelRotation)) * me.longestLabelWidth) + (tickFontSize * tallestLabelHeightInLines) + (lineSpace * tallestLabelHeightInLines); + var labelHeight = (sinRotation * largestTextWidth) + + (tickFont.size * tallestLabelHeightInLines) + + (lineSpace * tallestLabelHeightInLines); minSize.height = Math.min(me.maxHeight, minSize.height + labelHeight); - me.ctx.font = tickLabelFont; + me.ctx.font = tickFont.font; - var firstLabelWidth = me.ctx.measureText(me.ticks[0]).width; - var lastLabelWidth = me.ctx.measureText(me.ticks[me.ticks.length - 1]).width; + var firstTick = me.ticks[0]; + var firstLabelWidth = computeTextSize(me.ctx, firstTick, tickFont.font); + + var lastTick = me.ticks[me.ticks.length - 1]; + var lastLabelWidth = computeTextSize(me.ctx, lastTick, tickFont.font); // Ensure that our ticks are always inside the canvas. When rotated, ticks are right aligned which means that the right padding is dominated // by the font height - var cosRotation = Math.cos(helpers.toRadians(me.labelRotation)); - var sinRotation = Math.sin(helpers.toRadians(me.labelRotation)); me.paddingLeft = me.labelRotation !== 0 ? (cosRotation * firstLabelWidth) + 3 : firstLabelWidth / 2 + 3; // add 3 px to move away from canvas edges - me.paddingRight = me.labelRotation !== 0 ? (sinRotation * (tickFontSize / 2)) + 3 : lastLabelWidth / 2 + 3; // when rotated + me.paddingRight = me.labelRotation !== 0 ? (sinRotation * lineSpace) + 3 : lastLabelWidth / 2 + 3; // when rotated } else { // A vertical axis is more constrained by the width. Labels are the dominant factor here, so get that length first - // Account for padding - var mirror = tickOpts.mirror; - if (!mirror) { - largestTextWidth += me.options.ticks.padding; - } else { - // If mirrored text is on the inside so don't expand + + if (tickOpts.mirror) { largestTextWidth = 0; + } else { + largestTextWidth += me.options.ticks.padding; } - minSize.width += largestTextWidth; - me.paddingTop = tickFontSize / 2; - me.paddingBottom = tickFontSize / 2; + me.paddingTop = tickFont.size / 2; + me.paddingBottom = tickFont.size / 2; } } + me.handleMargins(); + + me.width = minSize.width; + me.height = minSize.height; + }, + + /** + * Handle margins and padding interactions + * @private + */ + handleMargins: function() { + var me = this; if (me.margins) { me.paddingLeft = Math.max(me.paddingLeft - me.margins.left, 0); me.paddingTop = Math.max(me.paddingTop - me.margins.top, 0); me.paddingRight = Math.max(me.paddingRight - me.margins.right, 0); me.paddingBottom = Math.max(me.paddingBottom - me.margins.bottom, 0); } - - me.width = minSize.width; - me.height = minSize.height; - }, + afterFit: function() { helpers.callCallback(this.options.afterFit, [this]); }, @@ -497,19 +492,14 @@ module.exports = function(Chart) { } var tickFontColor = helpers.getValueOrDefault(optionTicks.fontColor, globalDefaults.defaultFontColor); - var tickFontSize = helpers.getValueOrDefault(optionTicks.fontSize, globalDefaults.defaultFontSize); - var tickFontStyle = helpers.getValueOrDefault(optionTicks.fontStyle, globalDefaults.defaultFontStyle); - var tickFontFamily = helpers.getValueOrDefault(optionTicks.fontFamily, globalDefaults.defaultFontFamily); - var tickLabelFont = helpers.fontString(tickFontSize, tickFontStyle, tickFontFamily); + var tickFont = parseFontOptions(optionTicks); + var tl = gridLines.tickMarkLength; var borderDash = helpers.getValueOrDefault(gridLines.borderDash, globalDefaults.borderDash); var borderDashOffset = helpers.getValueOrDefault(gridLines.borderDashOffset, globalDefaults.borderDashOffset); var scaleLabelFontColor = helpers.getValueOrDefault(scaleLabel.fontColor, globalDefaults.defaultFontColor); - var scaleLabelFontSize = helpers.getValueOrDefault(scaleLabel.fontSize, globalDefaults.defaultFontSize); - var scaleLabelFontStyle = helpers.getValueOrDefault(scaleLabel.fontStyle, globalDefaults.defaultFontStyle); - var scaleLabelFontFamily = helpers.getValueOrDefault(scaleLabel.fontFamily, globalDefaults.defaultFontFamily); - var scaleLabelFont = helpers.fontString(scaleLabelFontSize, scaleLabelFontStyle, scaleLabelFontFamily); + var scaleLabelFont = parseFontOptions(scaleLabel); var labelRotationRadians = helpers.toRadians(me.labelRotation); var cosRotation = Math.cos(labelRotationRadians); @@ -679,17 +669,17 @@ module.exports = function(Chart) { context.save(); context.translate(itemToDraw.labelX, itemToDraw.labelY); context.rotate(itemToDraw.rotation); - context.font = tickLabelFont; + context.font = tickFont.font; context.textBaseline = itemToDraw.textBaseline; context.textAlign = itemToDraw.textAlign; var label = itemToDraw.label; if (helpers.isArray(label)) { - for (var i = 0, y = -(label.length - 1)*tickFontSize*0.75; i < label.length; ++i) { + for (var i = 0, y = 0; i < label.length; ++i) { // We just make sure the multiline element is a string here.. context.fillText('' + label[i], 0, y); // apply same lineSpacing as calculated @ L#320 - y += (tickFontSize * 1.5); + y += (tickFont.size * 1.5); } } else { context.fillText(label, 0, 0); @@ -706,10 +696,10 @@ module.exports = function(Chart) { if (isHorizontal) { scaleLabelX = me.left + ((me.right - me.left) / 2); // midpoint of the width - scaleLabelY = options.position === 'bottom' ? me.bottom - (scaleLabelFontSize / 2) : me.top + (scaleLabelFontSize / 2); + scaleLabelY = options.position === 'bottom' ? me.bottom - (scaleLabelFont.size / 2) : me.top + (scaleLabelFont.sie / 2); } else { var isLeft = options.position === 'left'; - scaleLabelX = isLeft ? me.left + (scaleLabelFontSize / 2) : me.right - (scaleLabelFontSize / 2); + scaleLabelX = isLeft ? me.left + (scaleLabelFont.size / 2) : me.right - (scaleLabelFont.size / 2); scaleLabelY = me.top + ((me.bottom - me.top) / 2); rotation = isLeft ? -0.5 * Math.PI : 0.5 * Math.PI; } @@ -720,7 +710,7 @@ module.exports = function(Chart) { context.textAlign = 'center'; context.textBaseline = 'middle'; context.fillStyle = scaleLabelFontColor; // render in correct colour - context.font = scaleLabelFont; + context.font = scaleLabelFont.font; context.fillText(scaleLabel.labelString, 0, 0); context.restore(); } From bdcdbc2abf2a57d49dfeb3bd52ee96757cfef555 Mon Sep 17 00:00:00 2001 From: Toshiki Saito Date: Sun, 27 Nov 2016 11:36:29 +0800 Subject: [PATCH 27/61] Fixed miscalculation of Bar width. for Bar and horizontalBar type, include stacked scale. issue #3589 --- src/controllers/controller.bar.js | 39 ++------ test/controller.bar.tests.js | 156 +++++++++++++++++++++++++++++- 2 files changed, 162 insertions(+), 33 deletions(-) diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index e9a85aabd22..e5070542e88 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -74,7 +74,7 @@ module.exports = function(Chart) { rectangle._datasetIndex = me.index; rectangle._index = index; - var ruler = me.getRuler(index); + var ruler = me.getRuler(index); // The index argument for compatible rectangle._model = { x: me.calculateBarX(index, me.index, ruler), y: reset ? scaleBase : me.calculateBarY(index, me.index), @@ -121,29 +121,17 @@ module.exports = function(Chart) { return yScale.getBasePixel(); }, - getRuler: function(index) { + getRuler: function() { var me = this; var meta = me.getMeta(); var xScale = me.getScaleForId(meta.xAxisID); var datasetCount = me.getBarCount(); - var tickWidth; - - if (xScale.options.type === 'category') { - tickWidth = xScale.getPixelForTick(index + 1) - xScale.getPixelForTick(index); - } else { - // Average width - tickWidth = xScale.width / xScale.ticks.length; - } + var tickWidth = xScale.width / xScale.ticks.length; var categoryWidth = tickWidth * xScale.options.categoryPercentage; var categorySpacing = (tickWidth - (tickWidth * xScale.options.categoryPercentage)) / 2; var fullBarWidth = categoryWidth / datasetCount; - if (xScale.ticks.length !== me.chart.data.labels.length) { - var perc = xScale.ticks.length / me.chart.data.labels.length; - fullBarWidth = fullBarWidth * perc; - } - var barWidth = fullBarWidth * xScale.options.barPercentage; var barSpacing = fullBarWidth - (fullBarWidth * xScale.options.barPercentage); @@ -163,7 +151,7 @@ module.exports = function(Chart) { if (xScale.options.barThickness) { return xScale.options.barThickness; } - return xScale.options.stacked ? ruler.categoryWidth : ruler.barWidth; + return xScale.options.stacked ? ruler.categoryWidth * xScale.options.barPercentage : ruler.barWidth; }, // Get bar index from the given dataset index accounting for the fact that not all bars are visible @@ -346,7 +334,7 @@ module.exports = function(Chart) { rectangle._datasetIndex = me.index; rectangle._index = index; - var ruler = me.getRuler(index); + var ruler = me.getRuler(index); // The index argument for compatible rectangle._model = { x: reset ? scaleBase : me.calculateBarX(index, me.index), y: me.calculateBarY(index, me.index, ruler), @@ -449,28 +437,17 @@ module.exports = function(Chart) { return xScale.getBasePixel(); }, - getRuler: function(index) { + getRuler: function() { var me = this; var meta = me.getMeta(); var yScale = me.getScaleForId(meta.yAxisID); var datasetCount = me.getBarCount(); - var tickHeight; - if (yScale.options.type === 'category') { - tickHeight = yScale.getPixelForTick(index + 1) - yScale.getPixelForTick(index); - } else { - // Average width - tickHeight = yScale.width / yScale.ticks.length; - } + var tickHeight = yScale.height / yScale.ticks.length; var categoryHeight = tickHeight * yScale.options.categoryPercentage; var categorySpacing = (tickHeight - (tickHeight * yScale.options.categoryPercentage)) / 2; var fullBarHeight = categoryHeight / datasetCount; - if (yScale.ticks.length !== me.chart.data.labels.length) { - var perc = yScale.ticks.length / me.chart.data.labels.length; - fullBarHeight = fullBarHeight * perc; - } - var barHeight = fullBarHeight * yScale.options.barPercentage; var barSpacing = fullBarHeight - (fullBarHeight * yScale.options.barPercentage); @@ -491,7 +468,7 @@ module.exports = function(Chart) { if (yScale.options.barThickness) { return yScale.options.barThickness; } - return yScale.options.stacked ? ruler.categoryHeight : ruler.barHeight; + return yScale.options.stacked ? ruler.categoryHeight * yScale.options.barPercentage : ruler.barHeight; }, calculateBarX: function(index, datasetIndex) { diff --git a/test/controller.bar.tests.js b/test/controller.bar.tests.js index 15fa00afed9..4df23047e01 100644 --- a/test/controller.bar.tests.js +++ b/test/controller.bar.tests.js @@ -240,7 +240,8 @@ describe('Bar controller tests', function() { scales: { xAxes: [{ type: 'category', - stacked: true + stacked: true, + barPercentage: 1 }], yAxes: [{ type: 'linear', @@ -296,7 +297,8 @@ describe('Bar controller tests', function() { scales: { xAxes: [{ type: 'category', - stacked: true + stacked: true, + barPercentage: 1 }], yAxes: [{ type: 'linear', @@ -487,4 +489,154 @@ describe('Bar controller tests', function() { expect(bar._model.borderColor).toBe('rgb(0, 255, 0)'); expect(bar._model.borderWidth).toBe(1.5); }); + + describe('Bar width', function() { + beforeEach(function() { + // 2 datasets + this.data = { + labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], + datasets: [{ + data: [10, 20, 30, 40, 50, 60, 70], + }, { + data: [10, 20, 30, 40, 50, 60, 70], + }] + }; + }); + + afterEach(function() { + var chart = window.acquireChart(this.config); + var meta = chart.getDatasetMeta(0); + var xScale = chart.scales[meta.xAxisID]; + + var categoryPercentage = xScale.options.categoryPercentage; + var barPercentage = xScale.options.barPercentage; + var stacked = xScale.options.stacked; + + var totalBarWidth = 0; + for (var i = 0; i < chart.data.datasets.length; i++) { + var bars = chart.getDatasetMeta(i).data; + for (var j = xScale.minIndex; j <= xScale.maxIndex; j++) { + totalBarWidth += bars[j]._model.width; + } + if (stacked) { + break; + } + } + + var actualValue = totalBarWidth; + var expectedValue = xScale.width * categoryPercentage * barPercentage; + expect(actualValue).toBeCloseToPixel(expectedValue); + + }); + + it('should correctly set bar width when min and max option is set.', function() { + this.config = { + type: 'bar', + data: this.data, + options: { + scales: { + xAxes: [{ + ticks: { + min: 'March', + max: 'May', + }, + }] + } + } + }; + }); + + it('should correctly set bar width when scale are stacked with min and max options.', function() { + this.config = { + type: 'bar', + data: this.data, + options: { + scales: { + xAxes: [{ + ticks: { + min: 'March', + max: 'May', + }, + stacked: true, + }] + } + } + }; + }); + }); + + describe('Bar height (horizontalBar type)', function() { + beforeEach(function() { + // 2 datasets + this.data = { + labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], + datasets: [{ + data: [10, 20, 30, 40, 50, 60, 70], + }, { + data: [10, 20, 30, 40, 50, 60, 70], + }] + }; + }); + + afterEach(function() { + var chart = window.acquireChart(this.config); + var meta = chart.getDatasetMeta(0); + var yScale = chart.scales[meta.yAxisID]; + + var categoryPercentage = yScale.options.categoryPercentage; + var barPercentage = yScale.options.barPercentage; + var stacked = yScale.options.stacked; + + var totalBarHeight = 0; + for (var i = 0; i < chart.data.datasets.length; i++) { + var bars = chart.getDatasetMeta(i).data; + for (var j = yScale.minIndex; j <= yScale.maxIndex; j++) { + totalBarHeight += bars[j]._model.height; + } + if (stacked) { + break; + } + } + + var actualValue = totalBarHeight; + var expectedValue = yScale.height * categoryPercentage * barPercentage; + expect(actualValue).toBeCloseToPixel(expectedValue); + + }); + + it('should correctly set bar height when min and max option is set.', function() { + this.config = { + type: 'horizontalBar', + data: this.data, + options: { + scales: { + yAxes: [{ + ticks: { + min: 'March', + max: 'May', + }, + }] + } + } + }; + }); + + it('should correctly set bar height when scale are stacked with min and max options.', function() { + this.config = { + type: 'horizontalBar', + data: this.data, + options: { + scales: { + yAxes: [{ + ticks: { + min: 'March', + max: 'May', + }, + stacked: true, + }] + } + } + }; + }); + }); }); From 5a24bfa500a35000de8eddce2ebdcb4efd6c1d41 Mon Sep 17 00:00:00 2001 From: SAiTO TOSHiKi Date: Sun, 4 Dec 2016 05:09:45 +0800 Subject: [PATCH 28/61] Implement clipping (#3658) Implements clipping of items outside the chart area. Resolves #3506 #3491 #2873 --- src/controllers/controller.bar.js | 2 ++ src/controllers/controller.line.js | 4 +++- src/core/core.canvasHelpers.js | 12 ++++++++++++ src/elements/element.point.js | 24 +++++++++++++++++++++++- 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index e5070542e88..bf1fb693793 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -229,12 +229,14 @@ module.exports = function(Chart) { var dataset = me.getDataset(); var i, len; + Chart.canvasHelpers.clipArea(me.chart.chart.ctx, me.chart.chartArea); for (i = 0, len = metaData.length; i < len; ++i) { var d = dataset.data[i]; if (d !== null && d !== undefined && !isNaN(d)) { metaData[i].transition(easingDecimal).draw(); } } + Chart.canvasHelpers.unclipArea(me.chart.chart.ctx); }, setHoverStyle: function(rectangle) { diff --git a/src/controllers/controller.line.js b/src/controllers/controller.line.js index 23d4eedc402..806ab4f3f58 100644 --- a/src/controllers/controller.line.js +++ b/src/controllers/controller.line.js @@ -292,14 +292,16 @@ module.exports = function(Chart) { points[i].transition(easingDecimal); } + Chart.canvasHelpers.clipArea(me.chart.chart.ctx, me.chart.chartArea); // Transition and Draw the line if (lineEnabled(me.getDataset(), me.chart.options)) { meta.dataset.transition(easingDecimal).draw(); } + Chart.canvasHelpers.unclipArea(me.chart.chart.ctx); // Draw the points for (i=0, ilen=points.length; i Date: Sat, 3 Dec 2016 17:42:33 -0500 Subject: [PATCH 29/61] Pass the hover event to the onHover event handler (#3669) Pass the hover event to the onHover event handler This makes the behavior of the `onHover` handler consistent with the `onClick` handler: ``` function(event, activeElements) { var chartInstance = this; } ``` --- docs/01-Chart-Configuration.md | 4 ++-- src/core/core.controller.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/01-Chart-Configuration.md b/docs/01-Chart-Configuration.md index 55f7ddb6eb9..0b353d3a8bd 100644 --- a/docs/01-Chart-Configuration.md +++ b/docs/01-Chart-Configuration.md @@ -81,7 +81,7 @@ responsive | Boolean | true | Resizes the chart canvas when its container does. responsiveAnimationDuration | Number | 0 | Duration in milliseconds it takes to animate to new size after a resize event. maintainAspectRatio | Boolean | true | Maintain the original canvas aspect ratio `(width / height)` when resizing events | Array[String] | `["mousemove", "mouseout", "click", "touchstart", "touchmove", "touchend"]` | Events that the chart should listen to for tooltips and hovering -onClick | Function | null | Called if the event is of type 'mouseup' or 'click'. Called in the context of the chart and passed an array of active elements +onClick | Function | null | Called if the event is of type 'mouseup' or 'click'. Called in the context of the chart and passed the event and an array of active elements legendCallback | Function | ` function (chart) { }` | Function to generate a legend. Receives the chart object to generate a legend from. Default implementation returns an HTML string. onResize | Function | null | Called when a resize occurs. Gets passed two arguments: the chart instance and the new size. @@ -310,7 +310,7 @@ Name | Type | Default | Description mode | String | 'nearest' | Sets which elements appear in the tooltip. See [Interaction Modes](#interaction-modes) for details intersect | Boolean | true | if true, the hover mode only applies when the mouse position intersects an item on the chart animationDuration | Number | 400 | Duration in milliseconds it takes to animate hover style changes -onHover | Function | null | Called when any of the events fire. Called in the context of the chart and passed an array of active elements (bars, points, etc) +onHover | Function | null | Called when any of the events fire. Called in the context of the chart and passed the event and an array of active elements (bars, points, etc) ### Interaction Modes When configuring interaction with the graph via hover or tooltips, a number of different modes are available. diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 1abc81075da..411021db485 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -796,7 +796,7 @@ module.exports = function(Chart) { // On Hover hook if (hoverOptions.onHover) { - hoverOptions.onHover.call(me, me.active); + hoverOptions.onHover.call(me, e, me.active); } if (e.type === 'mouseup' || e.type === 'click') { From 6aec98bf8b05f4ad40a4a4b08486bb7452229fce Mon Sep 17 00:00:00 2001 From: Wang Shenwei Date: Tue, 6 Dec 2016 21:05:04 +0800 Subject: [PATCH 30/61] Correct document for Interaction Modes #3676 (#3684) 'x-axis' Behaves like 'index' mode with intersect = false --- docs/01-Chart-Configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/01-Chart-Configuration.md b/docs/01-Chart-Configuration.md index 0b353d3a8bd..865a7558379 100644 --- a/docs/01-Chart-Configuration.md +++ b/docs/01-Chart-Configuration.md @@ -324,7 +324,7 @@ nearest | Gets the item that is nearest to the point. The nearest item is determ single (deprecated) | Finds the first item that intersects the point and returns it. Behaves like 'nearest' mode with intersect = true. label (deprecated) | See `'index'` mode index | Finds item at the same index. If the `intersect` setting is true, the first intersecting item is used to determine the index in the data. If `intersect` false the nearest item is used to determine the index. -x-axis (deprecated) | Behaves like `'index'` mode with `intersect = true` +x-axis (deprecated) | Behaves like `'index'` mode with `intersect = false` dataset | Finds items in the same dataset. If the `intersect` setting is true, the first intersecting item is used to determine the index in the data. If `intersect` false the nearest item is used to determine the index. x | Returns all items that would intersect based on the `X` coordinate of the position only. Would be useful for a vertical cursor implementation. Note that this only applies to cartesian charts y | Returns all items that would intersect based on the `Y` coordinate of the position. This would be useful for a horizontal cursor implementation. Note that this only applies to cartesian charts. From 75d15ff98b859dacc59520ba6943c99b00bd2ab6 Mon Sep 17 00:00:00 2001 From: etimberg Date: Sat, 3 Dec 2016 22:21:25 -0500 Subject: [PATCH 31/61] Fix newly introduced drawing bug when tick marks are not drawn --- src/core/core.scale.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/core.scale.js b/src/core/core.scale.js index 1cc434f638d..57aefd1dad8 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -494,7 +494,7 @@ module.exports = function(Chart) { var tickFontColor = helpers.getValueOrDefault(optionTicks.fontColor, globalDefaults.defaultFontColor); var tickFont = parseFontOptions(optionTicks); - var tl = gridLines.tickMarkLength; + var tl = gridLines.drawTicks ? gridLines.tickMarkLength : 0; var borderDash = helpers.getValueOrDefault(gridLines.borderDash, globalDefaults.borderDash); var borderDashOffset = helpers.getValueOrDefault(gridLines.borderDashOffset, globalDefaults.borderDashOffset); From 6f40d04964c621bf5903720c4a0be12de66df236 Mon Sep 17 00:00:00 2001 From: etimberg Date: Sat, 3 Dec 2016 22:21:48 -0500 Subject: [PATCH 32/61] Fix infinite loop in logarithmic tick generation --- src/core/core.ticks.js | 30 ++++++++++++++++++------------ test/scale.logarithmic.tests.js | 4 ++-- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/core/core.ticks.js b/src/core/core.ticks.js index f11f1192bdf..72441f3a553 100644 --- a/src/core/core.ticks.js +++ b/src/core/core.ticks.js @@ -109,27 +109,33 @@ module.exports = function(Chart) { // the graph var tickVal = getValueOrDefault(generationOptions.min, Math.pow(10, Math.floor(helpers.log10(dataRange.min)))); - while (tickVal < dataRange.max) { - ticks.push(tickVal); + var endExp = Math.floor(helpers.log10(dataRange.max)); + var endSignificand = Math.ceil(dataRange.max / Math.pow(10, endExp)); + var exp; + var significand; - var exp; - var significand; + if (tickVal === 0) { + exp = Math.floor(helpers.log10(dataRange.minNotZero)); + significand = Math.floor(dataRange.minNotZero / Math.pow(10, exp)); - if (tickVal === 0) { - exp = Math.floor(helpers.log10(dataRange.minNotZero)); - significand = Math.round(dataRange.minNotZero / Math.pow(10, exp)); - } else { - exp = Math.floor(helpers.log10(tickVal)); - significand = Math.floor(tickVal / Math.pow(10, exp)) + 1; - } + 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 = getValueOrDefault(generationOptions.max, tickVal); ticks.push(lastTick); diff --git a/test/scale.logarithmic.tests.js b/test/scale.logarithmic.tests.js index 64732d833b5..f5ee6eae994 100644 --- a/test/scale.logarithmic.tests.js +++ b/test/scale.logarithmic.tests.js @@ -343,7 +343,7 @@ describe('Logarithmic Scale tests', function() { data: [10, 5, 1, 5, 78, 100] }, { yAxisID: 'yScale1', - data: [-1000, 1000], + data: [0, 1000], }, { type: 'bar', yAxisID: 'yScale0', @@ -383,7 +383,7 @@ describe('Logarithmic Scale tests', function() { type: 'bar' }, { yAxisID: 'yScale1', - data: [-1000, 1000], + data: [0, 1000], type: 'bar' }, { yAxisID: 'yScale0', From 34d26ea49755cc41f294d7eb36a4324d628022aa Mon Sep 17 00:00:00 2001 From: Tarqwyn Date: Tue, 6 Dec 2016 14:43:29 +0000 Subject: [PATCH 33/61] Fix bug when calculating if steps fit into scale as a whole number then smal floating point errors make the consition pass false --- src/core/core.helpers.js | 4 ++++ src/core/core.ticks.js | 4 ++-- test/core.helpers.tests.js | 5 +++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index 506ad900397..bfbcf23d5f5 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -237,6 +237,10 @@ module.exports = function(Chart) { helpers.almostEquals = function(x, y, epsilon) { return Math.abs(x - y) < epsilon; }; + helpers.almostWhole = function(x, epsilon) { + var rounded = Math.round(x); + return (((rounded - epsilon) < x) && ((rounded + epsilon) > x)); + }; helpers.max = function(array) { return array.reduce(function(max, value) { if (!isNaN(value)) { diff --git a/src/core/core.ticks.js b/src/core/core.ticks.js index 72441f3a553..8d84f1f236a 100644 --- a/src/core/core.ticks.js +++ b/src/core/core.ticks.js @@ -67,8 +67,8 @@ module.exports = function(Chart) { // If min, max and stepSize is set and they make an evenly spaced scale use it. if (generationOptions.min && generationOptions.max && generationOptions.stepSize) { - var minMaxDeltaDivisibleByStepSize = ((generationOptions.max - generationOptions.min) % generationOptions.stepSize) === 0; - if (minMaxDeltaDivisibleByStepSize) { + // 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; } diff --git a/test/core.helpers.tests.js b/test/core.helpers.tests.js index 75e0baf900e..296958051d6 100644 --- a/test/core.helpers.tests.js +++ b/test/core.helpers.tests.js @@ -301,6 +301,11 @@ describe('Core helper tests', function() { expect(helpers.almostEquals(1e30, 1e30 + Number.EPSILON, 2 * Number.EPSILON)).toBe(true); }); + it('should correctly determine if a numbers are essentially whole', function() { + expect(helpers.almostWhole(0.99999, 0.0001)).toBe(true); + expect(helpers.almostWhole(0.9, 0.0001)).toBe(false); + }); + it('should generate integer ids', function() { var uid = helpers.uid(); expect(uid).toEqual(jasmine.any(Number)); From a5d9a02d5bf399f9ec442ba718b5d9c21a54552c Mon Sep 17 00:00:00 2001 From: SAiTO TOSHiKi Date: Fri, 16 Dec 2016 15:15:14 +0800 Subject: [PATCH 34/61] Update core.scale.js Change sie to size. --- src/core/core.scale.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/core.scale.js b/src/core/core.scale.js index 57aefd1dad8..0ea82451c60 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -696,7 +696,7 @@ module.exports = function(Chart) { if (isHorizontal) { scaleLabelX = me.left + ((me.right - me.left) / 2); // midpoint of the width - scaleLabelY = options.position === 'bottom' ? me.bottom - (scaleLabelFont.size / 2) : me.top + (scaleLabelFont.sie / 2); + scaleLabelY = options.position === 'bottom' ? me.bottom - (scaleLabelFont.size / 2) : me.top + (scaleLabelFont.size / 2); } else { var isLeft = options.position === 'left'; scaleLabelX = isLeft ? me.left + (scaleLabelFont.size / 2) : me.right - (scaleLabelFont.size / 2); From 00d3c5a282a553aeacf79e40cccabd19a8298805 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Fri, 16 Dec 2016 22:20:18 -0500 Subject: [PATCH 35/61] fix linting --- test/scale.logarithmic.tests.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/test/scale.logarithmic.tests.js b/test/scale.logarithmic.tests.js index f5ee6eae994..f7e35b209ba 100644 --- a/test/scale.logarithmic.tests.js +++ b/test/scale.logarithmic.tests.js @@ -77,15 +77,13 @@ describe('Logarithmic Scale tests', function() { }, { id: 'yScale1', type: 'logarithmic' - }, - { - id: 'yScale2', - type: 'logarithmic' - }, - { - id: 'yScale3', - type: 'logarithmic' - }] + }, { + id: 'yScale2', + type: 'logarithmic' + }, { + id: 'yScale3', + type: 'logarithmic' + }] } } }); From 18f77db362d59091a6abd2e89b9097394db358a2 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Fri, 16 Dec 2016 22:24:12 -0500 Subject: [PATCH 36/61] fix linting again --- test/scale.logarithmic.tests.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/test/scale.logarithmic.tests.js b/test/scale.logarithmic.tests.js index f7e35b209ba..016737c71ee 100644 --- a/test/scale.logarithmic.tests.js +++ b/test/scale.logarithmic.tests.js @@ -138,11 +138,10 @@ describe('Logarithmic Scale tests', function() { }, { id: 'yScale2', type: 'logarithmic' - }, - { - id: 'yScale3', - type: 'logarithmic' - }] + }, { + id: 'yScale3', + type: 'logarithmic' + }] } } }); From e249de7162acb432f3ae24b7485a992e7f9b3b3f Mon Sep 17 00:00:00 2001 From: etimberg Date: Sat, 17 Dec 2016 18:50:06 -0500 Subject: [PATCH 37/61] fix options in getDatasetAtEvent --- src/core/core.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 411021db485..61f99b0eb56 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -628,7 +628,7 @@ module.exports = function(Chart) { }, getDatasetAtEvent: function(e) { - return Chart.Interaction.modes.dataset(this, e); + return Chart.Interaction.modes.dataset(this, e, {intersect: true}); }, getDatasetMeta: function(datasetIndex) { From 3187a788e17f79ad69da509748aeb64cdbac48f0 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Thu, 3 Nov 2016 22:40:47 +0100 Subject: [PATCH 38/61] Add support for local plugins and plugin options Plugins can now be declared in the chart `config.plugins` array and will only be applied to the associated chart(s), after the globally registered plugins. Plugin specific options are now scoped under the `config.options.plugins` options. Hooks now receive the chart instance as first argument and the plugin options as last argument. --- src/chart.js | 2 +- src/core/core.controller.js | 28 ++-- src/core/core.plugin.js | 94 +++++++++-- test/core.plugin.tests.js | 312 ++++++++++++++++++++++++++++++------ test/mockContext.js | 1 + 5 files changed, 362 insertions(+), 75 deletions(-) diff --git a/src/chart.js b/src/chart.js index 2c5e628264c..7c490e7eabc 100644 --- a/src/chart.js +++ b/src/chart.js @@ -5,13 +5,13 @@ var Chart = require('./core/core.js')(); require('./core/core.helpers')(Chart); require('./core/core.canvasHelpers')(Chart); +require('./core/core.plugin.js')(Chart); require('./core/core.element')(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.plugin.js')(Chart); require('./core/core.ticks.js')(Chart); require('./core/core.scale')(Chart); require('./core/core.title')(Chart); diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 61f99b0eb56..4e28773880e 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -258,7 +258,7 @@ module.exports = function(Chart) { var me = this; // Before init plugin notification - Chart.plugins.notify('beforeInit', [me]); + Chart.plugins.notify(me, 'beforeInit'); me.bindEvents(); @@ -273,7 +273,7 @@ module.exports = function(Chart) { me.update(); // After init plugin notification - Chart.plugins.notify('afterInit', [me]); + Chart.plugins.notify(me, 'afterInit'); return me; }, @@ -315,7 +315,7 @@ module.exports = function(Chart) { if (!silent) { // Notify any plugins about the resize var newSize = {width: newWidth, height: newHeight}; - Chart.plugins.notify('resize', [me, newSize]); + Chart.plugins.notify(me, 'resize', [newSize]); // Notify of resize if (me.options.onResize) { @@ -460,7 +460,7 @@ module.exports = function(Chart) { var me = this; updateConfig(me); - Chart.plugins.notify('beforeUpdate', [me]); + Chart.plugins.notify(me, 'beforeUpdate'); // In case the entire data object changed me.tooltip._data = me.data; @@ -476,7 +476,7 @@ module.exports = function(Chart) { Chart.layoutService.update(me, me.chart.width, me.chart.height); // Apply changes to the datasets that require the scales to have been calculated i.e BorderColor changes - Chart.plugins.notify('afterScaleUpdate', [me]); + Chart.plugins.notify(me, 'afterScaleUpdate'); // Can only reset the new controllers after the scales have been updated helpers.each(newControllers, function(controller) { @@ -486,7 +486,7 @@ module.exports = function(Chart) { me.updateDatasets(); // Do this before render so that any plugins that need final scale updates can use it - Chart.plugins.notify('afterUpdate', [me]); + Chart.plugins.notify(me, 'afterUpdate'); if (me._bufferedRender) { me._bufferedRequest = { @@ -530,18 +530,18 @@ module.exports = function(Chart) { var me = this; var i, ilen; - if (Chart.plugins.notify('beforeDatasetsUpdate', [me])) { + if (Chart.plugins.notify(me, 'beforeDatasetsUpdate')) { for (i = 0, ilen = me.data.datasets.length; i < ilen; ++i) { me.getDatasetMeta(i).controller.update(); } - Chart.plugins.notify('afterDatasetsUpdate', [me]); + Chart.plugins.notify(me, 'afterDatasetsUpdate'); } }, render: function(duration, lazy) { var me = this; - Chart.plugins.notify('beforeRender', [me]); + Chart.plugins.notify(me, 'beforeRender'); var animationOptions = me.options.animation; if (animationOptions && ((typeof duration !== 'undefined' && duration !== 0) || (typeof duration === 'undefined' && animationOptions.duration !== 0))) { @@ -577,7 +577,7 @@ module.exports = function(Chart) { var easingDecimal = ease || 1; me.clear(); - Chart.plugins.notify('beforeDraw', [me, easingDecimal]); + Chart.plugins.notify(me, 'beforeDraw', [easingDecimal]); // Draw all the scales helpers.each(me.boxes, function(box) { @@ -587,7 +587,7 @@ module.exports = function(Chart) { me.scale.draw(); } - Chart.plugins.notify('beforeDatasetsDraw', [me, easingDecimal]); + Chart.plugins.notify(me, 'beforeDatasetsDraw', [easingDecimal]); // Draw each dataset via its respective controller (reversed to support proper line stacking) helpers.each(me.data.datasets, function(dataset, datasetIndex) { @@ -596,12 +596,12 @@ module.exports = function(Chart) { } }, me, true); - Chart.plugins.notify('afterDatasetsDraw', [me, easingDecimal]); + Chart.plugins.notify(me, 'afterDatasetsDraw', [easingDecimal]); // Finally draw the tooltip me.tooltip.transition(easingDecimal).draw(); - Chart.plugins.notify('afterDraw', [me, easingDecimal]); + Chart.plugins.notify(me, 'afterDraw', [easingDecimal]); }, // Get the single element that was clicked on @@ -701,7 +701,7 @@ module.exports = function(Chart) { me.chart.ctx = null; } - Chart.plugins.notify('destroy', [me]); + Chart.plugins.notify(me, 'destroy'); delete Chart.instances[me.id]; }, diff --git a/src/core/core.plugin.js b/src/core/core.plugin.js index fe6ac31f170..657c85a3545 100644 --- a/src/core/core.plugin.js +++ b/src/core/core.plugin.js @@ -2,7 +2,10 @@ module.exports = function(Chart) { - var noop = Chart.helpers.noop; + var helpers = Chart.helpers; + var noop = helpers.noop; + + Chart.defaults.global.plugins = {}; /** * The plugin service singleton @@ -10,8 +13,20 @@ module.exports = function(Chart) { * @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). @@ -23,6 +38,8 @@ module.exports = function(Chart) { p.push(plugin); } }); + + this._cacheId++; }, /** @@ -37,6 +54,8 @@ module.exports = function(Chart) { p.splice(idx, 1); } }); + + this._cacheId++; }, /** @@ -45,6 +64,7 @@ module.exports = function(Chart) { */ clear: function() { this._plugins = []; + this._cacheId++; }, /** @@ -66,28 +86,78 @@ module.exports = function(Chart) { }, /** - * Calls registered plugins on the specified extension, with the given args. This - * method immediately returns as soon as a plugin explicitly returns false. The + * Calls enabled plugins for chart, on the specified extension 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 chart instance for which plugins should be called. * @param {String} extension the name of the plugin method to call (e.g. 'beforeUpdate'). * @param {Array} [args] extra arguments to apply to the extension call. * @returns {Boolean} false if any of the plugins return false, else returns true. */ - notify: function(extension, args) { - var plugins = this._plugins; - var ilen = plugins.length; - var i, plugin; + notify: function(chart, extension, args) { + var descriptors = this.descriptors(chart); + var ilen = descriptors.length; + var i, descriptor, plugin, params, method; for (i=0; i Date: Thu, 8 Dec 2016 20:56:33 -0500 Subject: [PATCH 39/61] fix stacked bars on logarithmic axes --- src/controllers/controller.bar.js | 20 ++++++----- src/core/core.scale.js | 9 +++-- test/controller.bar.tests.js | 57 +++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 11 deletions(-) diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index bf1fb693793..fc2f1c0c662 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -99,7 +99,8 @@ module.exports = function(Chart) { var me = this; var meta = me.getMeta(); var yScale = me.getScaleForId(meta.yAxisID); - var base = 0; + var base = yScale.getBaseValue(); + var original = base; if (yScale.options.stacked) { var chart = me.chart; @@ -111,7 +112,7 @@ module.exports = function(Chart) { var currentDsMeta = chart.getDatasetMeta(i); if (currentDsMeta.bar && currentDsMeta.yAxisID === yScale.id && chart.isDatasetVisible(i)) { var currentVal = Number(currentDs.data[index]); - base += value < 0 ? Math.min(currentVal, 0) : Math.max(currentVal, 0); + base += value < 0 ? Math.min(currentVal, original) : Math.max(currentVal, original); } } @@ -197,8 +198,9 @@ module.exports = function(Chart) { if (yScale.options.stacked) { - var sumPos = 0, - sumNeg = 0; + var base = yScale.getBaseValue(); + var sumPos = base, + sumNeg = base; for (var i = 0; i < datasetIndex; i++) { var ds = me.chart.data.datasets[i]; @@ -417,7 +419,8 @@ module.exports = function(Chart) { var me = this; var meta = me.getMeta(); var xScale = me.getScaleForId(meta.xAxisID); - var base = 0; + var base = xScale.getBaseValue(); + var originalBase = base; if (xScale.options.stacked) { var chart = me.chart; @@ -429,7 +432,7 @@ module.exports = function(Chart) { var currentDsMeta = chart.getDatasetMeta(i); if (currentDsMeta.bar && currentDsMeta.xAxisID === xScale.id && chart.isDatasetVisible(i)) { var currentVal = Number(currentDs.data[index]); - base += value < 0 ? Math.min(currentVal, 0) : Math.max(currentVal, 0); + base += value < 0 ? Math.min(currentVal, originalBase) : Math.max(currentVal, originalBase); } } @@ -481,8 +484,9 @@ module.exports = function(Chart) { if (xScale.options.stacked) { - var sumPos = 0, - sumNeg = 0; + var base = xScale.getBaseValue(); + var sumPos = base, + sumNeg = base; for (var i = 0; i < datasetIndex; i++) { var ds = me.chart.data.datasets[i]; diff --git a/src/core/core.scale.js b/src/core/core.scale.js index 0ea82451c60..06047e4e39a 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -454,15 +454,18 @@ module.exports = function(Chart) { }, getBasePixel: function() { + return this.getPixelForValue(this.getBaseValue()); + }, + + getBaseValue: function() { var me = this; var min = me.min; var max = me.max; - return me.getPixelForValue( - me.beginAtZero? 0: + return me.beginAtZero ? 0: min < 0 && max < 0? max : min > 0 && max > 0? min : - 0); + 0; }, // Actually draw the scale on the canvas diff --git a/test/controller.bar.tests.js b/test/controller.bar.tests.js index 4df23047e01..fea88633ce9 100644 --- a/test/controller.bar.tests.js +++ b/test/controller.bar.tests.js @@ -337,6 +337,63 @@ describe('Bar controller tests', function() { }); }); + it('should update elements when the scales are stacked and the y axis is logarithmic', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [10, 100, 10, 100], + label: 'dataset1' + }, { + data: [100, 10, 0, 100], + label: 'dataset2' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + scales: { + xAxes: [{ + type: 'category', + stacked: true, + barPercentage: 1 + }], + yAxes: [{ + type: 'logarithmic', + stacked: true + }] + } + } + }); + + var meta0 = chart.getDatasetMeta(0); + + [ + {b: 484, w: 92, x: 94, y: 379}, + {b: 484, w: 92, x: 208, y: 122}, + {b: 484, w: 92, x: 322, y: 379}, + {b: 484, w: 92, x: 436, y: 122} + ].forEach(function(values, i) { + expect(meta0.data[i]._model.base).toBeCloseToPixel(values.b); + expect(meta0.data[i]._model.width).toBeCloseToPixel(values.w); + expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); + }); + + var meta1 = chart.getDatasetMeta(1); + + [ + {b: 379, w: 92, x: 94, y: 109}, + {b: 122, w: 92, x: 208, y: 109}, + {b: 379, w: 92, x: 322, y: 379}, + {b: 122, w: 92, x: 436, y: 25} + ].forEach(function(values, i) { + expect(meta1.data[i]._model.base).toBeCloseToPixel(values.b); + expect(meta1.data[i]._model.width).toBeCloseToPixel(values.w); + expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta1.data[i]._model.y).toBeCloseToPixel(values.y); + }); + }); + it('should draw all bars', function() { var chart = window.acquireChart({ type: 'bar', From 64b5def774e43490932633decfff055dfb01ccab Mon Sep 17 00:00:00 2001 From: SAiTO TOSHiKi Date: Mon, 19 Dec 2016 01:49:43 +0800 Subject: [PATCH 40/61] Fix : samples (line-stacked-area.html & step-size.html) (#3717) Fix : samples line-stacked-area.html:Changed j-query code to javascript step-size.html:Fixed buttons not working --- samples/line/line-stacked-area.html | 2 +- samples/scales/linear/step-size.html | 59 ++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/samples/line/line-stacked-area.html b/samples/line/line-stacked-area.html index 9621c14d511..041c9a84b72 100644 --- a/samples/line/line-stacked-area.html +++ b/samples/line/line-stacked-area.html @@ -121,7 +121,7 @@ }; document.getElementById('randomizeData').addEventListener('click', function() { - $.each(config.data.datasets, function(i, dataset) { + config.data.datasets.forEach(function(dataset) { dataset.data = dataset.data.map(function() { return randomScalingFactor(); }); diff --git a/samples/scales/linear/step-size.html b/samples/scales/linear/step-size.html index d38f3e1f0bb..ced0b6c3ca3 100644 --- a/samples/scales/linear/step-size.html +++ b/samples/scales/linear/step-size.html @@ -110,6 +110,65 @@ var ctx = document.getElementById("canvas").getContext("2d"); window.myLine = new Chart(ctx, config); }; + + document.getElementById('randomizeData').addEventListener('click', function() { + config.data.datasets.forEach(function(dataset) { + dataset.data = dataset.data.map(function() { + return randomScalingFactor(); + }); + }); + + window.myLine.update(); + }); + + var colorNames = Object.keys(window.chartColors); + document.getElementById('addDataset').addEventListener('click', function() { + var colorName = colorNames[config.data.datasets.length % colorNames.length]; + var newColor = window.chartColors[colorName]; + var newDataset = { + label: 'Dataset ' + config.data.datasets.length, + backgroundColor: newColor, + borderColor: newColor, + data: [], + fill: false + }; + + for (var index = 0; index < config.data.labels.length; ++index) { + newDataset.data.push(randomScalingFactor()); + } + + config.data.datasets.push(newDataset); + window.myLine.update(); + }); + + document.getElementById('addData').addEventListener('click', function() { + if (config.data.datasets.length > 0) { + var month = MONTHS[config.data.labels.length % MONTHS.length]; + config.data.labels.push(month); + + config.data.datasets.forEach(function(dataset) { + dataset.data.push(randomScalingFactor()); + }); + + window.myLine.update(); + } + }); + + document.getElementById('removeDataset').addEventListener('click', function() { + config.data.datasets.splice(0, 1); + window.myLine.update(); + }); + + document.getElementById('removeData').addEventListener('click', function() { + config.data.labels.splice(-1, 1); // remove the label first + + config.data.datasets.forEach(function(dataset, datasetIndex) { + dataset.data.pop(); + }); + + window.myLine.update(); + }); + From 5387c48bd83db8f33e7a6b147b260dc43714db6c Mon Sep 17 00:00:00 2001 From: SAiTO TOSHiKi Date: Tue, 20 Dec 2016 22:01:07 +0800 Subject: [PATCH 41/61] Fix bar draw issue with `borderWidth`. (#3680) Fix bar draw issue. 1. `Chart.elements.Rectangle.draw` function supports both horizontal and vertical bar. 2. Corrected bar position at minus. 3. Adjust bar size when `borderWidth` is set. 4. Adjust bar size when `borderSkipped` is set. 5. Adjust `borderWidth` with value near 0(base). 6. Update test. --- src/controllers/controller.bar.js | 58 +-------------------------- src/elements/element.rectangle.js | 66 +++++++++++++++++++++++-------- test/element.rectangle.tests.js | 4 +- 3 files changed, 53 insertions(+), 75 deletions(-) diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index fc2f1c0c662..ecf43b19bce 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -84,6 +84,7 @@ module.exports = function(Chart) { datasetLabel: dataset.label, // Appearance + horizontal: false, base: reset ? scaleBase : me.calculateBarBase(me.index, index), width: me.calculateBarWidth(ruler), backgroundColor: custom.backgroundColor ? custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, rectangleElementOptions.backgroundColor), @@ -348,6 +349,7 @@ module.exports = function(Chart) { datasetLabel: dataset.label, // Appearance + horizontal: true, base: reset ? scaleBase : me.calculateBarBase(me.index, index), height: me.calculateBarHeight(ruler), backgroundColor: custom.backgroundColor ? custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, rectangleElementOptions.backgroundColor), @@ -355,62 +357,6 @@ module.exports = function(Chart) { borderColor: custom.borderColor ? custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, rectangleElementOptions.borderColor), borderWidth: custom.borderWidth ? custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, rectangleElementOptions.borderWidth) }; - rectangle.draw = function() { - var ctx = this._chart.ctx; - var vm = this._view; - - var halfHeight = vm.height / 2, - topY = vm.y - halfHeight, - bottomY = vm.y + halfHeight, - right = vm.base - (vm.base - vm.x), - halfStroke = vm.borderWidth / 2; - - // Canvas doesn't allow us to stroke inside the width so we can - // adjust the sizes to fit if we're setting a stroke on the line - if (vm.borderWidth) { - topY += halfStroke; - bottomY -= halfStroke; - right += halfStroke; - } - - ctx.beginPath(); - - ctx.fillStyle = vm.backgroundColor; - ctx.strokeStyle = vm.borderColor; - ctx.lineWidth = vm.borderWidth; - - // Corner points, from bottom-left to bottom-right clockwise - // | 1 2 | - // | 0 3 | - var corners = [ - [vm.base, bottomY], - [vm.base, topY], - [right, topY], - [right, bottomY] - ]; - - // Find first (starting) corner with fallback to 'bottom' - var borders = ['bottom', 'left', 'top', 'right']; - var startCorner = borders.indexOf(vm.borderSkipped, 0); - if (startCorner === -1) { - startCorner = 0; - } - - function cornerAt(cornerIndex) { - return corners[(startCorner + cornerIndex) % 4]; - } - - // Draw rectangle from 'startCorner' - ctx.moveTo.apply(ctx, cornerAt(0)); - for (var i = 1; i < 4; i++) { - ctx.lineTo.apply(ctx, cornerAt(i)); - } - - ctx.fill(); - if (vm.borderWidth) { - ctx.stroke(); - } - }; rectangle.pivot(); }, diff --git a/src/elements/element.rectangle.js b/src/elements/element.rectangle.js index 427916791ed..c3b81976140 100644 --- a/src/elements/element.rectangle.js +++ b/src/elements/element.rectangle.js @@ -53,39 +53,71 @@ module.exports = function(Chart) { draw: function() { var ctx = this._chart.ctx; var vm = this._view; - - var halfWidth = vm.width / 2, - leftX = vm.x - halfWidth, - rightX = vm.x + halfWidth, - top = vm.base - (vm.base - vm.y), - halfStroke = vm.borderWidth / 2; + var left, right, top, bottom, signX, signY, borderSkipped; + var borderWidth = vm.borderWidth; + + if (!vm.horizontal) { + // bar + left = vm.x - vm.width / 2; + right = vm.x + vm.width / 2; + top = vm.y; + bottom = vm.base; + signX = 1; + signY = bottom > top? 1: -1; + borderSkipped = vm.borderSkipped || 'bottom'; + } else { + // horizontal bar + left = vm.base; + right = vm.x; + top = vm.y - vm.height / 2; + bottom = vm.y + vm.height / 2; + signX = right > left? 1: -1; + signY = 1; + borderSkipped = vm.borderSkipped || 'left'; + } // Canvas doesn't allow us to stroke inside the width so we can // adjust the sizes to fit if we're setting a stroke on the line - if (vm.borderWidth) { - leftX += halfStroke; - rightX -= halfStroke; - top += halfStroke; + if (borderWidth) { + // borderWidth shold be less than bar width and bar height. + var barSize = Math.min(Math.abs(left - right), Math.abs(top - bottom)); + borderWidth = borderWidth > barSize? barSize: borderWidth; + var halfStroke = borderWidth / 2; + // Adjust borderWidth when bar top position is near vm.base(zero). + var borderLeft = left + (borderSkipped !== 'left'? halfStroke * signX: 0); + var borderRight = right + (borderSkipped !== 'right'? -halfStroke * signX: 0); + var borderTop = top + (borderSkipped !== 'top'? halfStroke * signY: 0); + var borderBottom = bottom + (borderSkipped !== 'bottom'? -halfStroke * signY: 0); + // not become a vertical line? + if (borderLeft !== borderRight) { + top = borderTop; + bottom = borderBottom; + } + // not become a horizontal line? + if (borderTop !== borderBottom) { + left = borderLeft; + right = borderRight; + } } ctx.beginPath(); ctx.fillStyle = vm.backgroundColor; ctx.strokeStyle = vm.borderColor; - ctx.lineWidth = vm.borderWidth; + ctx.lineWidth = borderWidth; // Corner points, from bottom-left to bottom-right clockwise // | 1 2 | // | 0 3 | var corners = [ - [leftX, vm.base], - [leftX, top], - [rightX, top], - [rightX, vm.base] + [left, bottom], + [left, top], + [right, top], + [right, bottom] ]; // Find first (starting) corner with fallback to 'bottom' var borders = ['bottom', 'left', 'top', 'right']; - var startCorner = borders.indexOf(vm.borderSkipped, 0); + var startCorner = borders.indexOf(borderSkipped, 0); if (startCorner === -1) { startCorner = 0; } @@ -104,7 +136,7 @@ module.exports = function(Chart) { } ctx.fill(); - if (vm.borderWidth) { + if (borderWidth) { ctx.stroke(); } }, diff --git a/test/element.rectangle.tests.js b/test/element.rectangle.tests.js index b833862bd6f..e72117f5a36 100644 --- a/test/element.rectangle.tests.js +++ b/test/element.rectangle.tests.js @@ -207,10 +207,10 @@ describe('Rectangle element tests', function() { args: [8.5, 0] }, { name: 'lineTo', - args: [8.5, 15.5] + args: [8.5, 14.5] // This is a minus bar. Not 15.5 }, { name: 'lineTo', - args: [11.5, 15.5] + args: [11.5, 14.5] }, { name: 'lineTo', args: [11.5, 0] From ecc35c527b8d91c2bdbc2cdc97ef667adfe7e3a3 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Wed, 21 Dec 2016 10:22:05 -0500 Subject: [PATCH 42/61] Refactoring to put browser specific code in a new class (#3718) Refactoring to put browser specific code in a new class, BrowserPlatform. BrowserPlatform implements IPlatform. Chart.Platform is the constructor for the platform object that is attached to the chart instance. Plugins are notified about the event using the `onEvent` call. The legend plugin was converted to use onEvent instead of the older private `handleEvent` method. Wrote test to check that plugins are notified about events --- docs/09-Advanced.md | 8 + src/chart.js | 3 + src/core/core.controller.js | 156 ++------------ src/core/core.interaction.js | 31 ++- src/core/core.legend.js | 20 +- src/core/core.tooltip.js | 7 +- src/platforms/platform.dom.js | 237 +++++++++++++++++++++ test/core.controller.tests.js | 297 --------------------------- test/platform.dom.tests.js | 373 ++++++++++++++++++++++++++++++++++ 9 files changed, 677 insertions(+), 455 deletions(-) create mode 100644 src/platforms/platform.dom.js create mode 100644 test/platform.dom.tests.js diff --git a/docs/09-Advanced.md b/docs/09-Advanced.md index f76ac4b0d97..4c677c904d4 100644 --- a/docs/09-Advanced.md +++ b/docs/09-Advanced.md @@ -410,6 +410,7 @@ Plugins will be called at the following times * After datasets draw * Resize * Before an animation is started +* When an event occurs on the canvas (mousemove, click, etc). This requires the `options.events` property handled Plugins should derive from Chart.PluginBase and implement the following interface ```javascript @@ -437,6 +438,13 @@ Plugins should derive from Chart.PluginBase and implement the following interfac afterDatasetsDraw: function(chartInstance, easing) { }, destroy: function(chartInstance) { } + + /** + * Called when an event occurs on the chart + * @param e {Core.Event} the Chart.js wrapper around the native event. e.native is the original event + * @return {Boolean} true if the chart is changed and needs to re-render + */ + onEvent: function(chartInstance, e) {} } ``` diff --git a/src/chart.js b/src/chart.js index 7c490e7eabc..186d07a42f5 100644 --- a/src/chart.js +++ b/src/chart.js @@ -19,6 +19,9 @@ require('./core/core.legend')(Chart); require('./core/core.interaction')(Chart); require('./core/core.tooltip')(Chart); +// By default, we only load the browser platform. +Chart.platform = require('./platforms/platform.dom')(Chart); + require('./elements/element.arc')(Chart); require('./elements/element.line')(Chart); require('./elements/element.point')(Chart); diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 4e28773880e..e64cb3a9dde 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -14,140 +14,6 @@ module.exports = function(Chart) { // Controllers available for dataset visualization eg. bar, line, slice, etc. Chart.controllers = {}; - /** - * The "used" size is the final value of a dimension property after all calculations have - * been performed. This method uses the computed style of `element` but returns undefined - * if the computed style is not expressed in pixels. That can happen in some cases where - * `element` has a size relative to its parent and this last one is not yet displayed, - * for example because of `display: none` on a parent node. - * TODO(SB) Move this method in the upcoming core.platform class. - * @see https://developer.mozilla.org/en-US/docs/Web/CSS/used_value - * @returns {Number} Size in pixels or undefined if unknown. - */ - function readUsedSize(element, property) { - var value = helpers.getStyle(element, property); - var matches = value && value.match(/(\d+)px/); - return matches? Number(matches[1]) : undefined; - } - - /** - * Initializes the canvas style and render size without modifying the canvas display size, - * since responsiveness is handled by the controller.resize() method. The config is used - * to determine the aspect ratio to apply in case no explicit height has been specified. - * TODO(SB) Move this method in the upcoming core.platform class. - */ - function initCanvas(canvas, config) { - var style = canvas.style; - - // NOTE(SB) canvas.getAttribute('width') !== canvas.width: in the first case it - // returns null or '' if no explicit value has been set to the canvas attribute. - var renderHeight = canvas.getAttribute('height'); - var renderWidth = canvas.getAttribute('width'); - - // Chart.js modifies some canvas values that we want to restore on destroy - canvas._chartjs = { - initial: { - height: renderHeight, - width: renderWidth, - style: { - display: style.display, - height: style.height, - width: style.width - } - } - }; - - // Force canvas to display as block to avoid extra space caused by inline - // elements, which would interfere with the responsive resize process. - // https://github.com/chartjs/Chart.js/issues/2538 - style.display = style.display || 'block'; - - if (renderWidth === null || renderWidth === '') { - var displayWidth = readUsedSize(canvas, 'width'); - if (displayWidth !== undefined) { - canvas.width = displayWidth; - } - } - - if (renderHeight === null || renderHeight === '') { - if (canvas.style.height === '') { - // If no explicit render height and style height, let's apply the aspect ratio, - // which one can be specified by the user but also by charts as default option - // (i.e. options.aspectRatio). If not specified, use canvas aspect ratio of 2. - canvas.height = canvas.width / (config.options.aspectRatio || 2); - } else { - var displayHeight = readUsedSize(canvas, 'height'); - if (displayWidth !== undefined) { - canvas.height = displayHeight; - } - } - } - - return canvas; - } - - /** - * Restores the canvas initial state, such as render/display sizes and style. - * TODO(SB) Move this method in the upcoming core.platform class. - */ - function releaseCanvas(canvas) { - if (!canvas._chartjs) { - return; - } - - var initial = canvas._chartjs.initial; - ['height', 'width'].forEach(function(prop) { - var value = initial[prop]; - if (value === undefined || value === null) { - canvas.removeAttribute(prop); - } else { - canvas.setAttribute(prop, value); - } - }); - - helpers.each(initial.style || {}, function(value, key) { - canvas.style[key] = value; - }); - - // The canvas render size might have been changed (and thus the state stack discarded), - // we can't use save() and restore() to restore the initial state. So make sure that at - // least the canvas context is reset to the default state by setting the canvas width. - // https://www.w3.org/TR/2011/WD-html5-20110525/the-canvas-element.html - canvas.width = canvas.width; - - delete canvas._chartjs; - } - - /** - * TODO(SB) Move this method in the upcoming core.platform class. - */ - function acquireContext(item, config) { - if (typeof item === 'string') { - item = document.getElementById(item); - } else if (item.length) { - // Support for array based queries (such as jQuery) - item = item[0]; - } - - if (item && item.canvas) { - // Support for any object associated to a canvas (including a context2d) - item = item.canvas; - } - - if (item instanceof HTMLCanvasElement) { - // To prevent canvas fingerprinting, some add-ons undefine the getContext - // method, for example: https://github.com/kkapsner/CanvasBlocker - // https://github.com/chartjs/Chart.js/issues/2807 - var context = item.getContext && item.getContext('2d'); - if (context instanceof CanvasRenderingContext2D) { - initCanvas(item, config); - return context; - } - } - - return null; - } - /** * Initializes the given config with global and chart default values. */ @@ -197,7 +63,7 @@ module.exports = function(Chart) { config = initConfig(config); - var context = acquireContext(item, config); + var context = Chart.platform.acquireContext(item, config); var canvas = context && context.canvas; var height = canvas && canvas.height; var width = canvas && canvas.width; @@ -696,7 +562,7 @@ module.exports = function(Chart) { helpers.unbindEvents(me, me.events); helpers.removeResizeListener(canvas.parentNode); helpers.clear(me.chart); - releaseCanvas(canvas); + Chart.platform.releaseContext(me.chart.ctx); me.chart.canvas = null; me.chart.ctx = null; } @@ -742,7 +608,6 @@ module.exports = function(Chart) { eventHandler: function(e) { var me = this; - var legend = me.legend; var tooltip = me.tooltip; var hoverOptions = me.options.hover; @@ -750,9 +615,12 @@ module.exports = function(Chart) { me._bufferedRender = true; me._bufferedRequest = null; - var changed = me.handleEvent(e); - changed |= legend && legend.handleEvent(e); - changed |= tooltip && tooltip.handleEvent(e); + // Create platform agnostic chart event using platform specific code + var chartEvent = Chart.platform.createEvent(e, me.chart); + + var changed = me.handleEvent(chartEvent); + changed |= tooltip && tooltip.handleEvent(chartEvent); + changed |= Chart.plugins.notify(me, 'onEvent', [chartEvent]); var bufferedRequest = me._bufferedRequest; if (bufferedRequest) { @@ -776,7 +644,7 @@ module.exports = function(Chart) { /** * Handle an event * @private - * param e {Event} the event to handle + * param e {Core.Event} the event to handle * @return {Boolean} true if the chart needs to re-render */ handleEvent: function(e) { @@ -796,12 +664,14 @@ module.exports = function(Chart) { // On Hover hook if (hoverOptions.onHover) { - hoverOptions.onHover.call(me, e, me.active); + // Need to call with native event here to not break backwards compatibility + hoverOptions.onHover.call(me, e.native, me.active); } if (e.type === 'mouseup' || e.type === 'click') { if (options.onClick) { - options.onClick.call(me, e, me.active); + // Use e.native here for backwards compatibility + options.onClick.call(me, e.native, me.active); } } diff --git a/src/core/core.interaction.js b/src/core/core.interaction.js index aacdda19c38..0888fa9591b 100644 --- a/src/core/core.interaction.js +++ b/src/core/core.interaction.js @@ -3,6 +3,23 @@ module.exports = function(Chart) { var helpers = Chart.helpers; + /** + * Helper function to get relative position for an event + * @param e {Event|Core.Event} the event to get the position for + * @param chart {chart} the chart + * @returns {Point} the event position + */ + function getRelativePosition(e, chart) { + if (e.native) { + return { + x: e.x, + y: e.y + }; + } + + return helpers.getRelativePosition(e, chart); + } + /** * Helper function to traverse all of the visible elements in the chart * @param chart {chart} the chart @@ -82,7 +99,7 @@ module.exports = function(Chart) { } function indexMode(chart, e, options) { - var position = helpers.getRelativePosition(e, chart.chart); + var position = getRelativePosition(e, chart.chart); var distanceMetric = function(pt1, pt2) { return Math.abs(pt1.x - pt2.x); }; @@ -125,7 +142,7 @@ module.exports = function(Chart) { // Helper function for different modes modes: { single: function(chart, e) { - var position = helpers.getRelativePosition(e, chart.chart); + var position = getRelativePosition(e, chart.chart); var elements = []; parseVisibleItems(chart, function(element) { @@ -166,7 +183,7 @@ module.exports = function(Chart) { * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned */ dataset: function(chart, e, options) { - var position = helpers.getRelativePosition(e, chart.chart); + var position = getRelativePosition(e, chart.chart); var items = options.intersect ? getIntersectItems(chart, position) : getNearestItems(chart, position, false); if (items.length > 0) { @@ -193,7 +210,7 @@ module.exports = function(Chart) { * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned */ point: function(chart, e) { - var position = helpers.getRelativePosition(e, chart.chart); + var position = getRelativePosition(e, chart.chart); return getIntersectItems(chart, position); }, @@ -206,7 +223,7 @@ module.exports = function(Chart) { * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned */ nearest: function(chart, e, options) { - var position = helpers.getRelativePosition(e, chart.chart); + var position = getRelativePosition(e, chart.chart); var nearestItems = getNearestItems(chart, position, options.intersect); // We have multiple items at the same distance from the event. Now sort by smallest @@ -238,7 +255,7 @@ module.exports = function(Chart) { * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned */ x: function(chart, e, options) { - var position = helpers.getRelativePosition(e, chart.chart); + var position = getRelativePosition(e, chart.chart); var items = []; var intersectsItem = false; @@ -269,7 +286,7 @@ module.exports = function(Chart) { * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned */ y: function(chart, e, options) { - var position = helpers.getRelativePosition(e, chart.chart); + var position = getRelativePosition(e, chart.chart); var items = []; var intersectsItem = false; diff --git a/src/core/core.legend.js b/src/core/core.legend.js index 4ab51ba5dbf..45f51d05923 100644 --- a/src/core/core.legend.js +++ b/src/core/core.legend.js @@ -439,7 +439,7 @@ module.exports = function(Chart) { /** * Handle an event * @private - * @param e {Event} the event to handle + * @param e {Core.Event} the event to handle * @return {Boolean} true if a change occured */ handleEvent: function(e) { @@ -460,9 +460,9 @@ module.exports = function(Chart) { return; } - var position = helpers.getRelativePosition(e, me.chart.chart), - x = position.x, - y = position.y; + // Chart event already has relative position in it + var x = e.x, + 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 @@ -473,11 +473,13 @@ module.exports = function(Chart) { if (x >= hitBox.left && x <= hitBox.left + hitBox.width && y >= hitBox.top && y <= hitBox.top + hitBox.height) { // Touching an element if (type === 'click') { - opts.onClick.call(me, e, me.legendItems[i]); + // use e.native for backwards compatibility + opts.onClick.call(me, e.native, me.legendItems[i]); changed = true; break; } else if (type === 'mousemove') { - opts.onHover.call(me, e, me.legendItems[i]); + // use e.native for backwards compatibility + opts.onHover.call(me, e.native, me.legendItems[i]); changed = true; break; } @@ -523,6 +525,12 @@ module.exports = function(Chart) { Chart.layoutService.removeBox(chartInstance, chartInstance.legend); delete chartInstance.legend; } + }, + onEvent: function(chartInstance, e) { + var legend = chartInstance.legend; + if (legend) { + legend.handleEvent(e); + } } }); }; diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index d99f2302ff5..c1ac7830739 100755 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -763,7 +763,7 @@ module.exports = function(Chart) { /** * Handle an event * @private - * @param e {Event} the event to handle + * @param e {Core.Event} the event to handle * @returns {Boolean} true if the tooltip changed */ handleEvent: function(e) { @@ -785,7 +785,10 @@ module.exports = function(Chart) { me._lastActive = me._active; if (options.enabled || options.custom) { - me._eventPosition = helpers.getRelativePosition(e, me._chart); + me._eventPosition = { + x: e.x, + y: e.y + }; var model = me._model; me.update(true); diff --git a/src/platforms/platform.dom.js b/src/platforms/platform.dom.js new file mode 100644 index 00000000000..fb41f88259f --- /dev/null +++ b/src/platforms/platform.dom.js @@ -0,0 +1,237 @@ +'use strict'; + +/** + * @interface IPlatform + * Allows abstracting platform dependencies away from the chart + */ +/** + * Creates a chart.js event from a platform specific event + * @method IPlatform#createEvent + * @param e {Event} : the platform event to translate + * @returns {Core.Event} chart.js event + */ +/** + * @method IPlatform#acquireContext + * @param item {Object} the context or canvas to use + * @param config {ChartOptions} the chart options + * @returns {CanvasRenderingContext2D} a context2d instance implementing the w3c Canvas 2D context API standard. + */ +/** + * @method IPlatform#releaseContext + * @param context {CanvasRenderingContext2D} the context to release. This is the item returned by @see {@link IPlatform#acquireContext} + */ + +// Chart.Platform implementation for targeting a web browser +module.exports = function(Chart) { + var helpers = Chart.helpers; + + /* + * Key is the browser event type + * Chart.js internal events are: + * mouseenter + * mousedown + * mousemove + * mouseup + * mouseout + * click + * dblclick + * contextmenu + * keydown + * keypress + * keyup + */ + var typeMap = { + // Mouse events + mouseenter: 'mouseenter', + mousedown: 'mousedown', + mousemove: 'mousemove', + mouseup: 'mouseup', + mouseout: 'mouseout', + mouseleave: 'mouseout', + click: 'click', + dblclick: 'dblclick', + contextmenu: 'contextmenu', + + // Touch events + touchstart: 'mousedown', + touchmove: 'mousemove', + touchend: 'mouseup', + + // Pointer events + pointerenter: 'mouseenter', + pointerdown: 'mousedown', + pointermove: 'mousemove', + pointerup: 'mouseup', + pointerleave: 'mouseout', + pointerout: 'mouseout', + + // Key events + keydown: 'keydown', + keypress: 'keypress', + keyup: 'keyup', + }; + + /** + * The "used" size is the final value of a dimension property after all calculations have + * been performed. This method uses the computed style of `element` but returns undefined + * if the computed style is not expressed in pixels. That can happen in some cases where + * `element` has a size relative to its parent and this last one is not yet displayed, + * for example because of `display: none` on a parent node. + * @see https://developer.mozilla.org/en-US/docs/Web/CSS/used_value + * @returns {Number} Size in pixels or undefined if unknown. + */ + function readUsedSize(element, property) { + var value = helpers.getStyle(element, property); + var matches = value && value.match(/(\d+)px/); + return matches? Number(matches[1]) : undefined; + } + + /** + * Initializes the canvas style and render size without modifying the canvas display size, + * since responsiveness is handled by the controller.resize() method. The config is used + * to determine the aspect ratio to apply in case no explicit height has been specified. + */ + function initCanvas(canvas, config) { + var style = canvas.style; + + // NOTE(SB) canvas.getAttribute('width') !== canvas.width: in the first case it + // returns null or '' if no explicit value has been set to the canvas attribute. + var renderHeight = canvas.getAttribute('height'); + var renderWidth = canvas.getAttribute('width'); + + // Chart.js modifies some canvas values that we want to restore on destroy + canvas._chartjs = { + initial: { + height: renderHeight, + width: renderWidth, + style: { + display: style.display, + height: style.height, + width: style.width + } + } + }; + + // Force canvas to display as block to avoid extra space caused by inline + // elements, which would interfere with the responsive resize process. + // https://github.com/chartjs/Chart.js/issues/2538 + style.display = style.display || 'block'; + + if (renderWidth === null || renderWidth === '') { + var displayWidth = readUsedSize(canvas, 'width'); + if (displayWidth !== undefined) { + canvas.width = displayWidth; + } + } + + if (renderHeight === null || renderHeight === '') { + if (canvas.style.height === '') { + // If no explicit render height and style height, let's apply the aspect ratio, + // which one can be specified by the user but also by charts as default option + // (i.e. options.aspectRatio). If not specified, use canvas aspect ratio of 2. + canvas.height = canvas.width / (config.options.aspectRatio || 2); + } else { + var displayHeight = readUsedSize(canvas, 'height'); + if (displayWidth !== undefined) { + canvas.height = displayHeight; + } + } + } + + return canvas; + } + + return { + /** + * Creates a Chart.js event from a raw event + * @method BrowserPlatform#createEvent + * @implements IPlatform.createEvent + * @param e {Event} the raw event (such as a mouse event) + * @param chart {Chart} the chart to use + * @returns {Core.Event} the chart.js event for this event + */ + createEvent: function(e, chart) { + var relativePosition = helpers.getRelativePosition(e, chart); + return { + // allow access to the native event + native: e, + + // our interal event type + type: typeMap[e.type], + + // width and height of chart + width: chart.width, + height: chart.height, + + // Position relative to the canvas + x: relativePosition.x, + y: relativePosition.y + }; + }, + + /** + * @method BrowserPlatform#acquireContext + * @implements IPlatform#acquireContext + */ + acquireContext: function(item, config) { + if (typeof item === 'string') { + item = document.getElementById(item); + } else if (item.length) { + // Support for array based queries (such as jQuery) + item = item[0]; + } + + if (item && item.canvas) { + // Support for any object associated to a canvas (including a context2d) + item = item.canvas; + } + + if (item instanceof HTMLCanvasElement) { + // To prevent canvas fingerprinting, some add-ons undefine the getContext + // method, for example: https://github.com/kkapsner/CanvasBlocker + // https://github.com/chartjs/Chart.js/issues/2807 + var context = item.getContext && item.getContext('2d'); + if (context instanceof CanvasRenderingContext2D) { + initCanvas(item, config); + return context; + } + } + + return null; + }, + + /** + * Restores the canvas initial state, such as render/display sizes and style. + * @method BrowserPlatform#releaseContext + * @implements IPlatform#releaseContext + */ + releaseContext: function(context) { + var canvas = context.canvas; + if (!canvas._chartjs) { + return; + } + + var initial = canvas._chartjs.initial; + ['height', 'width'].forEach(function(prop) { + var value = initial[prop]; + if (value === undefined || value === null) { + canvas.removeAttribute(prop); + } else { + canvas.setAttribute(prop, value); + } + }); + + helpers.each(initial.style || {}, function(value, key) { + canvas.style[key] = value; + }); + + // The canvas render size might have been changed (and thus the state stack discarded), + // we can't use save() and restore() to restore the initial state. So make sure that at + // least the canvas context is reset to the default state by setting the canvas width. + // https://www.w3.org/TR/2011/WD-html5-20110525/the-canvas-element.html + canvas.width = canvas.width; + + delete canvas._chartjs; + } + }; +}; diff --git a/test/core.controller.tests.js b/test/core.controller.tests.js index 44e7d3d21ed..0e895615812 100644 --- a/test/core.controller.tests.js +++ b/test/core.controller.tests.js @@ -13,74 +13,6 @@ describe('Chart.Controller', function() { Chart.helpers.addEvent(content, state !== 'complete'? 'load' : 'resize', handler); } - describe('context acquisition', function() { - var canvasId = 'chartjs-canvas'; - - beforeEach(function() { - var canvas = document.createElement('canvas'); - canvas.setAttribute('id', canvasId); - window.document.body.appendChild(canvas); - }); - - afterEach(function() { - document.getElementById(canvasId).remove(); - }); - - // see https://github.com/chartjs/Chart.js/issues/2807 - it('should gracefully handle invalid item', function() { - var chart = new Chart('foobar'); - - expect(chart).not.toBeValidChart(); - - chart.destroy(); - }); - - it('should accept a DOM element id', function() { - var canvas = document.getElementById(canvasId); - var chart = new Chart(canvasId); - - expect(chart).toBeValidChart(); - expect(chart.chart.canvas).toBe(canvas); - expect(chart.chart.ctx).toBe(canvas.getContext('2d')); - - chart.destroy(); - }); - - it('should accept a canvas element', function() { - var canvas = document.getElementById(canvasId); - var chart = new Chart(canvas); - - expect(chart).toBeValidChart(); - expect(chart.chart.canvas).toBe(canvas); - expect(chart.chart.ctx).toBe(canvas.getContext('2d')); - - chart.destroy(); - }); - - it('should accept a canvas context2D', function() { - var canvas = document.getElementById(canvasId); - var context = canvas.getContext('2d'); - var chart = new Chart(context); - - expect(chart).toBeValidChart(); - expect(chart.chart.canvas).toBe(canvas); - expect(chart.chart.ctx).toBe(context); - - chart.destroy(); - }); - - it('should accept an array containing canvas', function() { - var canvas = document.getElementById(canvasId); - var chart = new Chart([canvas]); - - expect(chart).toBeValidChart(); - expect(chart.chart.canvas).toBe(canvas); - expect(chart.chart.ctx).toBe(canvas.getContext('2d')); - - chart.destroy(); - }); - }); - describe('config initialization', function() { it('should create missing config.data properties', function() { var chart = acquireChart({}); @@ -164,152 +96,7 @@ describe('Chart.Controller', function() { }); }); - describe('config.options.aspectRatio', function() { - it('should use default "global" aspect ratio for render and display sizes', function() { - var chart = acquireChart({ - options: { - responsive: false - } - }, { - canvas: { - style: 'width: 620px' - } - }); - - expect(chart).toBeChartOfSize({ - dw: 620, dh: 310, - rw: 620, rh: 310, - }); - }); - - it('should use default "chart" aspect ratio for render and display sizes', function() { - var chart = acquireChart({ - type: 'doughnut', - options: { - responsive: false - } - }, { - canvas: { - style: 'width: 425px' - } - }); - - expect(chart).toBeChartOfSize({ - dw: 425, dh: 425, - rw: 425, rh: 425, - }); - }); - - it('should use "user" aspect ratio for render and display sizes', function() { - var chart = acquireChart({ - options: { - responsive: false, - aspectRatio: 3 - } - }, { - canvas: { - style: 'width: 405px' - } - }); - - expect(chart).toBeChartOfSize({ - dw: 405, dh: 135, - rw: 405, rh: 135, - }); - }); - - it('should not apply aspect ratio when height specified', function() { - var chart = acquireChart({ - options: { - responsive: false, - aspectRatio: 3 - } - }, { - canvas: { - style: 'width: 400px; height: 410px' - } - }); - - expect(chart).toBeChartOfSize({ - dw: 400, dh: 410, - rw: 400, rh: 410, - }); - }); - }); - describe('config.options.responsive: false', function() { - it('should use default canvas size for render and display sizes', function() { - var chart = acquireChart({ - options: { - responsive: false - } - }, { - canvas: { - style: '' - } - }); - - expect(chart).toBeChartOfSize({ - dw: 300, dh: 150, - rw: 300, rh: 150, - }); - }); - - it('should use canvas attributes for render and display sizes', function() { - var chart = acquireChart({ - options: { - responsive: false - } - }, { - canvas: { - style: '', - width: 305, - height: 245, - } - }); - - expect(chart).toBeChartOfSize({ - dw: 305, dh: 245, - rw: 305, rh: 245, - }); - }); - - it('should use canvas style for render and display sizes (if no attributes)', function() { - var chart = acquireChart({ - options: { - responsive: false - } - }, { - canvas: { - style: 'width: 345px; height: 125px' - } - }); - - expect(chart).toBeChartOfSize({ - dw: 345, dh: 125, - rw: 345, rh: 125, - }); - }); - - it('should use attributes for the render size and style for the display size', function() { - var chart = acquireChart({ - options: { - responsive: false - } - }, { - canvas: { - style: 'width: 345px; height: 125px;', - width: 165, - height: 85, - } - }); - - expect(chart).toBeChartOfSize({ - dw: 345, dh: 125, - rw: 165, rh: 85, - }); - }); - it('should not inject the resizer element', function() { var chart = acquireChart({ options: { @@ -563,27 +350,6 @@ describe('Chart.Controller', function() { }); describe('config.options.responsive: true (maintainAspectRatio: true)', function() { - it('should fill parent width and use aspect ratio to calculate height', function() { - var chart = acquireChart({ - options: { - responsive: true, - maintainAspectRatio: true - } - }, { - canvas: { - style: 'width: 150px; height: 245px' - }, - wrapper: { - style: 'width: 300px; height: 350px' - } - }); - - expect(chart).toBeChartOfSize({ - dw: 300, dh: 490, - rw: 300, rh: 490, - }); - }); - it('should resize the canvas with correct aspect ratio when parent width changes', function(done) { var chart = acquireChart({ type: 'line', // AR == 2 @@ -714,69 +480,6 @@ describe('Chart.Controller', function() { }); describe('controller.destroy', function() { - it('should reset context to default values', function() { - var chart = acquireChart({}); - var context = chart.chart.ctx; - - chart.destroy(); - - // https://www.w3.org/TR/2dcontext/#conformance-requirements - Chart.helpers.each({ - fillStyle: '#000000', - font: '10px sans-serif', - lineJoin: 'miter', - lineCap: 'butt', - lineWidth: 1, - miterLimit: 10, - shadowBlur: 0, - shadowColor: 'rgba(0, 0, 0, 0)', - shadowOffsetX: 0, - shadowOffsetY: 0, - strokeStyle: '#000000', - textAlign: 'start', - textBaseline: 'alphabetic' - }, function(value, key) { - expect(context[key]).toBe(value); - }); - }); - - it('should restore canvas initial values', function(done) { - var chart = acquireChart({ - options: { - responsive: true, - maintainAspectRatio: false - } - }, { - canvas: { - width: 180, - style: 'width: 512px; height: 480px' - }, - wrapper: { - style: 'width: 450px; height: 450px; position: relative' - } - }); - - var canvas = chart.chart.canvas; - var wrapper = canvas.parentNode; - wrapper.style.width = '475px'; - waitForResize(chart, function() { - expect(chart).toBeChartOfSize({ - dw: 475, dh: 450, - rw: 475, rh: 450, - }); - - chart.destroy(); - - expect(canvas.getAttribute('width')).toBe('180'); - expect(canvas.getAttribute('height')).toBe(null); - expect(canvas.style.width).toBe('512px'); - expect(canvas.style.height).toBe('480px'); - expect(canvas.style.display).toBe(''); - - done(); - }); - }); - it('should remove the resizer element when responsive: true', function() { var chart = acquireChart({ options: { diff --git a/test/platform.dom.tests.js b/test/platform.dom.tests.js new file mode 100644 index 00000000000..f20b6e1ff68 --- /dev/null +++ b/test/platform.dom.tests.js @@ -0,0 +1,373 @@ +describe('Platform.dom', function() { + + function waitForResize(chart, callback) { + var resizer = chart.chart.canvas.parentNode._chartjs.resizer; + var content = resizer.contentWindow || resizer; + var state = content.document.readyState || 'complete'; + var handler = function() { + Chart.helpers.removeEvent(content, 'load', handler); + Chart.helpers.removeEvent(content, 'resize', handler); + setTimeout(callback, 50); + }; + + Chart.helpers.addEvent(content, state !== 'complete'? 'load' : 'resize', handler); + } + + describe('context acquisition', function() { + var canvasId = 'chartjs-canvas'; + + beforeEach(function() { + var canvas = document.createElement('canvas'); + canvas.setAttribute('id', canvasId); + window.document.body.appendChild(canvas); + }); + + afterEach(function() { + document.getElementById(canvasId).remove(); + }); + + // see https://github.com/chartjs/Chart.js/issues/2807 + it('should gracefully handle invalid item', function() { + var chart = new Chart('foobar'); + + expect(chart).not.toBeValidChart(); + + chart.destroy(); + }); + + it('should accept a DOM element id', function() { + var canvas = document.getElementById(canvasId); + var chart = new Chart(canvasId); + + expect(chart).toBeValidChart(); + expect(chart.chart.canvas).toBe(canvas); + expect(chart.chart.ctx).toBe(canvas.getContext('2d')); + + chart.destroy(); + }); + + it('should accept a canvas element', function() { + var canvas = document.getElementById(canvasId); + var chart = new Chart(canvas); + + expect(chart).toBeValidChart(); + expect(chart.chart.canvas).toBe(canvas); + expect(chart.chart.ctx).toBe(canvas.getContext('2d')); + + chart.destroy(); + }); + + it('should accept a canvas context2D', function() { + var canvas = document.getElementById(canvasId); + var context = canvas.getContext('2d'); + var chart = new Chart(context); + + expect(chart).toBeValidChart(); + expect(chart.chart.canvas).toBe(canvas); + expect(chart.chart.ctx).toBe(context); + + chart.destroy(); + }); + + it('should accept an array containing canvas', function() { + var canvas = document.getElementById(canvasId); + var chart = new Chart([canvas]); + + expect(chart).toBeValidChart(); + expect(chart.chart.canvas).toBe(canvas); + expect(chart.chart.ctx).toBe(canvas.getContext('2d')); + + chart.destroy(); + }); + }); + + describe('config.options.aspectRatio', function() { + it('should use default "global" aspect ratio for render and display sizes', function() { + var chart = acquireChart({ + options: { + responsive: false + } + }, { + canvas: { + style: 'width: 620px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 620, dh: 310, + rw: 620, rh: 310, + }); + }); + + it('should use default "chart" aspect ratio for render and display sizes', function() { + var chart = acquireChart({ + type: 'doughnut', + options: { + responsive: false + } + }, { + canvas: { + style: 'width: 425px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 425, dh: 425, + rw: 425, rh: 425, + }); + }); + + it('should use "user" aspect ratio for render and display sizes', function() { + var chart = acquireChart({ + options: { + responsive: false, + aspectRatio: 3 + } + }, { + canvas: { + style: 'width: 405px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 405, dh: 135, + rw: 405, rh: 135, + }); + }); + + it('should not apply aspect ratio when height specified', function() { + var chart = acquireChart({ + options: { + responsive: false, + aspectRatio: 3 + } + }, { + canvas: { + style: 'width: 400px; height: 410px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 400, dh: 410, + rw: 400, rh: 410, + }); + }); + }); + + describe('config.options.responsive: false', function() { + it('should use default canvas size for render and display sizes', function() { + var chart = acquireChart({ + options: { + responsive: false + } + }, { + canvas: { + style: '' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 300, dh: 150, + rw: 300, rh: 150, + }); + }); + + it('should use canvas attributes for render and display sizes', function() { + var chart = acquireChart({ + options: { + responsive: false + } + }, { + canvas: { + style: '', + width: 305, + height: 245, + } + }); + + expect(chart).toBeChartOfSize({ + dw: 305, dh: 245, + rw: 305, rh: 245, + }); + }); + + it('should use canvas style for render and display sizes (if no attributes)', function() { + var chart = acquireChart({ + options: { + responsive: false + } + }, { + canvas: { + style: 'width: 345px; height: 125px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 345, dh: 125, + rw: 345, rh: 125, + }); + }); + + it('should use attributes for the render size and style for the display size', function() { + var chart = acquireChart({ + options: { + responsive: false + } + }, { + canvas: { + style: 'width: 345px; height: 125px;', + width: 165, + height: 85, + } + }); + + expect(chart).toBeChartOfSize({ + dw: 345, dh: 125, + rw: 165, rh: 85, + }); + }); + }); + + describe('config.options.responsive: true (maintainAspectRatio: true)', function() { + it('should fill parent width and use aspect ratio to calculate height', function() { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: true + } + }, { + canvas: { + style: 'width: 150px; height: 245px' + }, + wrapper: { + style: 'width: 300px; height: 350px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 300, dh: 490, + rw: 300, rh: 490, + }); + }); + }); + + describe('controller.destroy', function() { + it('should reset context to default values', function() { + var chart = acquireChart({}); + var context = chart.chart.ctx; + + chart.destroy(); + + // https://www.w3.org/TR/2dcontext/#conformance-requirements + Chart.helpers.each({ + fillStyle: '#000000', + font: '10px sans-serif', + lineJoin: 'miter', + lineCap: 'butt', + lineWidth: 1, + miterLimit: 10, + shadowBlur: 0, + shadowColor: 'rgba(0, 0, 0, 0)', + shadowOffsetX: 0, + shadowOffsetY: 0, + strokeStyle: '#000000', + textAlign: 'start', + textBaseline: 'alphabetic' + }, function(value, key) { + expect(context[key]).toBe(value); + }); + }); + + it('should restore canvas initial values', function(done) { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + width: 180, + style: 'width: 512px; height: 480px' + }, + wrapper: { + style: 'width: 450px; height: 450px; position: relative' + } + }); + + var canvas = chart.chart.canvas; + var wrapper = canvas.parentNode; + wrapper.style.width = '475px'; + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 475, dh: 450, + rw: 475, rh: 450, + }); + + chart.destroy(); + + expect(canvas.getAttribute('width')).toBe('180'); + expect(canvas.getAttribute('height')).toBe(null); + expect(canvas.style.width).toBe('512px'); + expect(canvas.style.height).toBe('480px'); + expect(canvas.style.display).toBe(''); + + done(); + }); + }); + }); + + describe('event handling', function() { + it('should notify plugins about events', function() { + var notifiedEvent; + var plugin = { + onEvent: function(chart, e) { + notifiedEvent = e; + } + }; + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + responsive: true + }, + plugins: [plugin] + }); + + var node = chart.chart.canvas; + var rect = node.getBoundingClientRect(); + var clientX = (rect.left + rect.right) / 2; + var clientY = (rect.top + rect.bottom) / 2; + + var evt = new MouseEvent('click', { + view: window, + bubbles: true, + cancelable: true, + clientX: clientX, + clientY: clientY + }); + + // Manually trigger rather than having an async test + node.dispatchEvent(evt); + + // Check that notifiedEvent is correct + expect(notifiedEvent).not.toBe(undefined); + expect(notifiedEvent.native).toBe(evt); + + // Is type correctly translated + expect(notifiedEvent.type).toBe(evt.type); + + // Canvas width and height + expect(notifiedEvent.width).toBe(chart.chart.width); + expect(notifiedEvent.height).toBe(chart.chart.height); + + // Relative Position + expect(notifiedEvent.x).toBe(chart.chart.width / 2); + expect(notifiedEvent.y).toBe(chart.chart.height / 2); + }); + }); +}); From 6255131156268393d12bab6cff078b3b51a4af4a Mon Sep 17 00:00:00 2001 From: Timofey Rechkalov Date: Sat, 24 Dec 2016 05:24:36 +0500 Subject: [PATCH 43/61] Update 07-Pie-Doughnut-Chart.md Fixed example in pie chart docs. --- docs/07-Pie-Doughnut-Chart.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/07-Pie-Doughnut-Chart.md b/docs/07-Pie-Doughnut-Chart.md index 0b2cc115e43..99830132281 100644 --- a/docs/07-Pie-Doughnut-Chart.md +++ b/docs/07-Pie-Doughnut-Chart.md @@ -102,8 +102,10 @@ For example, we could have a doughnut chart that animates by scaling out from th ```javascript new Chart(ctx,{ type:"doughnut", - animation:{ - animateScale:true + options: { + animation:{ + animateScale:true + } } }); // This will create a chart with all of the default options, merged from the global config, From bf61b2c2973693164f7e2f784c36b38abe8fc9a6 Mon Sep 17 00:00:00 2001 From: Zach Panzarino Date: Sat, 31 Dec 2016 22:25:55 +0000 Subject: [PATCH 44/61] Happy new year! Updated copyright date to 2017 --- LICENSE.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/LICENSE.md b/LICENSE.md index dc93bcc36ce..620db307e3c 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,5 +1,6 @@ The MIT License (MIT) -Copyright (c) 2013-2016 Nick Downie + +Copyright (c) 2013-2017 Nick Downie 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: From eebaa84e72d82fb2ddf6075a7f21e0d1b9aa7b24 Mon Sep 17 00:00:00 2001 From: potatopeelings Date: Mon, 2 Jan 2017 01:36:01 +1100 Subject: [PATCH 45/61] Group stacked bar charts (#2643) (#3563) Group stacked bar charts (#2643) --- docs/04-Bar-Chart.md | 1 + samples/bar/bar-stacked-group.html | 105 ++++ src/controllers/controller.bar.js | 167 ++++-- src/scales/scale.linear.js | 36 +- src/scales/scale.logarithmic.js | 34 +- test/controller.bar.tests.js | 825 +++++++++++++++++++++++++++-- 6 files changed, 1045 insertions(+), 123 deletions(-) create mode 100644 samples/bar/bar-stacked-group.html diff --git a/docs/04-Bar-Chart.md b/docs/04-Bar-Chart.md index 777acc7a7e7..ef2cd3c19a6 100644 --- a/docs/04-Bar-Chart.md +++ b/docs/04-Bar-Chart.md @@ -49,6 +49,7 @@ borderSkipped | `String or Array` | Which edge to skip drawing the borde hoverBackgroundColor | `Color or Array` | Bar background color when hovered hoverBorderColor | `Color or Array` | Bar border color when hovered hoverBorderWidth | `Number or Array` | Border width of bar when hovered +stack | `String` | The ID of the group to which this dataset belongs to (when stacked, each group will be a separate stack) An example data object using these attributes is shown below. diff --git a/samples/bar/bar-stacked-group.html b/samples/bar/bar-stacked-group.html new file mode 100644 index 00000000000..074e3eac08c --- /dev/null +++ b/samples/bar/bar-stacked-group.html @@ -0,0 +1,105 @@ + + + + + Stacked Bar Chart with Groups + + + + + + +
+ +
+ + + + + diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index ecf43b19bce..1d41386b6d7 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -35,21 +35,33 @@ module.exports = function(Chart) { initialize: function(chart, datasetIndex) { Chart.DatasetController.prototype.initialize.call(this, chart, datasetIndex); + var me = this; + var meta = me.getMeta(); + var dataset = me.getDataset(); + + meta.stack = dataset.stack; // Use this to indicate that this is a bar dataset. - this.getMeta().bar = true; + meta.bar = true; }, - // Get the number of datasets that display bars. We use this to correctly calculate the bar width - getBarCount: function() { + // Correctly calculate the bar width accounting for stacks and the fact that not all bars are visible + getStackCount: function() { var me = this; - var barCount = 0; + var meta = me.getMeta(); + var yScale = me.getScaleForId(meta.yAxisID); + + var stacks = []; helpers.each(me.chart.data.datasets, function(dataset, datasetIndex) { - var meta = me.chart.getDatasetMeta(datasetIndex); - if (meta.bar && me.chart.isDatasetVisible(datasetIndex)) { - ++barCount; + var dsMeta = me.chart.getDatasetMeta(datasetIndex); + if (dsMeta.bar && me.chart.isDatasetVisible(datasetIndex) && + (yScale.options.stacked === false || + (yScale.options.stacked === true && stacks.indexOf(dsMeta.stack) === -1) || + (yScale.options.stacked === undefined && (dsMeta.stack === undefined || stacks.indexOf(dsMeta.stack) === -1)))) { + stacks.push(dsMeta.stack); } }, me); - return barCount; + + return stacks.length; }, update: function(reset) { @@ -103,7 +115,8 @@ module.exports = function(Chart) { var base = yScale.getBaseValue(); var original = base; - if (yScale.options.stacked) { + if ((yScale.options.stacked === true) || + (yScale.options.stacked === undefined && meta.stack !== undefined)) { var chart = me.chart; var datasets = chart.data.datasets; var value = Number(datasets[datasetIndex].data[index]); @@ -111,7 +124,8 @@ module.exports = function(Chart) { for (var i = 0; i < datasetIndex; i++) { var currentDs = datasets[i]; var currentDsMeta = chart.getDatasetMeta(i); - if (currentDsMeta.bar && currentDsMeta.yAxisID === yScale.id && chart.isDatasetVisible(i)) { + if (currentDsMeta.bar && currentDsMeta.yAxisID === yScale.id && chart.isDatasetVisible(i) && + meta.stack === currentDsMeta.stack) { var currentVal = Number(currentDs.data[index]); base += value < 0 ? Math.min(currentVal, original) : Math.max(currentVal, original); } @@ -127,18 +141,18 @@ module.exports = function(Chart) { var me = this; var meta = me.getMeta(); var xScale = me.getScaleForId(meta.xAxisID); - var datasetCount = me.getBarCount(); + var stackCount = me.getStackCount(); - var tickWidth = xScale.width / xScale.ticks.length; + var tickWidth = xScale.width / xScale.ticks.length; var categoryWidth = tickWidth * xScale.options.categoryPercentage; var categorySpacing = (tickWidth - (tickWidth * xScale.options.categoryPercentage)) / 2; - var fullBarWidth = categoryWidth / datasetCount; + var fullBarWidth = categoryWidth / stackCount; var barWidth = fullBarWidth * xScale.options.barPercentage; var barSpacing = fullBarWidth - (fullBarWidth * xScale.options.barPercentage); return { - datasetCount: datasetCount, + stackCount: stackCount, tickWidth: tickWidth, categoryWidth: categoryWidth, categorySpacing: categorySpacing, @@ -149,46 +163,50 @@ module.exports = function(Chart) { }, calculateBarWidth: function(ruler) { - var xScale = this.getScaleForId(this.getMeta().xAxisID); + var me = this; + var meta = me.getMeta(); + var xScale = me.getScaleForId(meta.xAxisID); if (xScale.options.barThickness) { return xScale.options.barThickness; } - return xScale.options.stacked ? ruler.categoryWidth * xScale.options.barPercentage : ruler.barWidth; + return ruler.barWidth; }, - // Get bar index from the given dataset index accounting for the fact that not all bars are visible - getBarIndex: function(datasetIndex) { - var barIndex = 0; - var meta, j; + // Get stack index from the given dataset index accounting for stacks and the fact that not all bars are visible + getStackIndex: function(datasetIndex) { + var me = this; + var meta = me.chart.getDatasetMeta(datasetIndex); + var yScale = me.getScaleForId(meta.yAxisID); + var dsMeta, j; + var stacks = [meta.stack]; for (j = 0; j < datasetIndex; ++j) { - meta = this.chart.getDatasetMeta(j); - if (meta.bar && this.chart.isDatasetVisible(j)) { - ++barIndex; + dsMeta = this.chart.getDatasetMeta(j); + if (dsMeta.bar && this.chart.isDatasetVisible(j) && + (yScale.options.stacked === false || + (yScale.options.stacked === true && stacks.indexOf(dsMeta.stack) === -1) || + (yScale.options.stacked === undefined && (dsMeta.stack === undefined || stacks.indexOf(dsMeta.stack) === -1)))) { + stacks.push(dsMeta.stack); } } - return barIndex; + return stacks.length - 1; }, calculateBarX: function(index, datasetIndex, ruler) { var me = this; var meta = me.getMeta(); var xScale = me.getScaleForId(meta.xAxisID); - var barIndex = me.getBarIndex(datasetIndex); + var stackIndex = me.getStackIndex(datasetIndex); var leftTick = xScale.getPixelForValue(null, index, datasetIndex, me.chart.isCombo); leftTick -= me.chart.isCombo ? (ruler.tickWidth / 2) : 0; - if (xScale.options.stacked) { - return leftTick + (ruler.categoryWidth / 2) + ruler.categorySpacing; - } - return leftTick + (ruler.barWidth / 2) + ruler.categorySpacing + - (ruler.barWidth * barIndex) + + (ruler.barWidth * stackIndex) + (ruler.barSpacing / 2) + - (ruler.barSpacing * barIndex); + (ruler.barSpacing * stackIndex); }, calculateBarY: function(index, datasetIndex) { @@ -197,16 +215,17 @@ module.exports = function(Chart) { var yScale = me.getScaleForId(meta.yAxisID); var value = Number(me.getDataset().data[index]); - if (yScale.options.stacked) { - + if (yScale.options.stacked || + (yScale.options.stacked === undefined && meta.stack !== undefined)) { var base = yScale.getBaseValue(); var sumPos = base, - sumNeg = base; + sumNeg = base; for (var i = 0; i < datasetIndex; i++) { var ds = me.chart.data.datasets[i]; var dsMeta = me.chart.getDatasetMeta(i); - if (dsMeta.bar && dsMeta.yAxisID === yScale.id && me.chart.isDatasetVisible(i)) { + if (dsMeta.bar && dsMeta.yAxisID === yScale.id && me.chart.isDatasetVisible(i) && + meta.stack === dsMeta.stack) { var stackedVal = Number(ds.data[index]); if (stackedVal < 0) { sumNeg += stackedVal || 0; @@ -324,6 +343,27 @@ module.exports = function(Chart) { }; Chart.controllers.horizontalBar = Chart.controllers.bar.extend({ + + // Correctly calculate the bar width accounting for stacks and the fact that not all bars are visible + getStackCount: function() { + var me = this; + var meta = me.getMeta(); + var xScale = me.getScaleForId(meta.xAxisID); + + var stacks = []; + helpers.each(me.chart.data.datasets, function(dataset, datasetIndex) { + var dsMeta = me.chart.getDatasetMeta(datasetIndex); + if (dsMeta.bar && me.chart.isDatasetVisible(datasetIndex) && + (xScale.options.stacked === false || + (xScale.options.stacked === true && stacks.indexOf(dsMeta.stack) === -1) || + (xScale.options.stacked === undefined && (dsMeta.stack === undefined || stacks.indexOf(dsMeta.stack) === -1)))) { + stacks.push(dsMeta.stack); + } + }, me); + + return stacks.length; + }, + updateElement: function(rectangle, index, reset) { var me = this; var meta = me.getMeta(); @@ -368,7 +408,8 @@ module.exports = function(Chart) { var base = xScale.getBaseValue(); var originalBase = base; - if (xScale.options.stacked) { + if (xScale.options.stacked || + (xScale.options.stacked === undefined && meta.stack !== undefined)) { var chart = me.chart; var datasets = chart.data.datasets; var value = Number(datasets[datasetIndex].data[index]); @@ -376,7 +417,8 @@ module.exports = function(Chart) { for (var i = 0; i < datasetIndex; i++) { var currentDs = datasets[i]; var currentDsMeta = chart.getDatasetMeta(i); - if (currentDsMeta.bar && currentDsMeta.xAxisID === xScale.id && chart.isDatasetVisible(i)) { + if (currentDsMeta.bar && currentDsMeta.xAxisID === xScale.id && chart.isDatasetVisible(i) && + meta.stack === currentDsMeta.stack) { var currentVal = Number(currentDs.data[index]); base += value < 0 ? Math.min(currentVal, originalBase) : Math.max(currentVal, originalBase); } @@ -392,18 +434,18 @@ module.exports = function(Chart) { var me = this; var meta = me.getMeta(); var yScale = me.getScaleForId(meta.yAxisID); - var datasetCount = me.getBarCount(); + var stackCount = me.getStackCount(); var tickHeight = yScale.height / yScale.ticks.length; var categoryHeight = tickHeight * yScale.options.categoryPercentage; var categorySpacing = (tickHeight - (tickHeight * yScale.options.categoryPercentage)) / 2; - var fullBarHeight = categoryHeight / datasetCount; + var fullBarHeight = categoryHeight / stackCount; var barHeight = fullBarHeight * yScale.options.barPercentage; var barSpacing = fullBarHeight - (fullBarHeight * yScale.options.barPercentage); return { - datasetCount: datasetCount, + stackCount: stackCount, tickHeight: tickHeight, categoryHeight: categoryHeight, categorySpacing: categorySpacing, @@ -415,11 +457,33 @@ module.exports = function(Chart) { calculateBarHeight: function(ruler) { var me = this; - var yScale = me.getScaleForId(me.getMeta().yAxisID); + var meta = me.getMeta(); + var yScale = me.getScaleForId(meta.yAxisID); if (yScale.options.barThickness) { return yScale.options.barThickness; } - return yScale.options.stacked ? ruler.categoryHeight * yScale.options.barPercentage : ruler.barHeight; + return ruler.barHeight; + }, + + // Get stack index from the given dataset index accounting for stacks and the fact that not all bars are visible + getStackIndex: function(datasetIndex) { + var me = this; + var meta = me.chart.getDatasetMeta(datasetIndex); + var xScale = me.getScaleForId(meta.xAxisID); + var dsMeta, j; + var stacks = [meta.stack]; + + for (j = 0; j < datasetIndex; ++j) { + dsMeta = this.chart.getDatasetMeta(j); + if (dsMeta.bar && this.chart.isDatasetVisible(j) && + (xScale.options.stacked === false || + (xScale.options.stacked === true && stacks.indexOf(dsMeta.stack) === -1) || + (xScale.options.stacked === undefined && (dsMeta.stack === undefined || stacks.indexOf(dsMeta.stack) === -1)))) { + stacks.push(dsMeta.stack); + } + } + + return stacks.length - 1; }, calculateBarX: function(index, datasetIndex) { @@ -428,16 +492,17 @@ module.exports = function(Chart) { var xScale = me.getScaleForId(meta.xAxisID); var value = Number(me.getDataset().data[index]); - if (xScale.options.stacked) { - + if (xScale.options.stacked || + (xScale.options.stacked === undefined && meta.stack !== undefined)) { var base = xScale.getBaseValue(); var sumPos = base, - sumNeg = base; + sumNeg = base; for (var i = 0; i < datasetIndex; i++) { var ds = me.chart.data.datasets[i]; var dsMeta = me.chart.getDatasetMeta(i); - if (dsMeta.bar && dsMeta.xAxisID === xScale.id && me.chart.isDatasetVisible(i)) { + if (dsMeta.bar && dsMeta.xAxisID === xScale.id && me.chart.isDatasetVisible(i) && + meta.stack === dsMeta.stack) { var stackedVal = Number(ds.data[index]); if (stackedVal < 0) { sumNeg += stackedVal || 0; @@ -460,20 +525,16 @@ module.exports = function(Chart) { var me = this; var meta = me.getMeta(); var yScale = me.getScaleForId(meta.yAxisID); - var barIndex = me.getBarIndex(datasetIndex); + var stackIndex = me.getStackIndex(datasetIndex); var topTick = yScale.getPixelForValue(null, index, datasetIndex, me.chart.isCombo); topTick -= me.chart.isCombo ? (ruler.tickHeight / 2) : 0; - if (yScale.options.stacked) { - return topTick + (ruler.categoryHeight / 2) + ruler.categorySpacing; - } - return topTick + (ruler.barHeight / 2) + ruler.categorySpacing + - (ruler.barHeight * barIndex) + + (ruler.barHeight * stackIndex) + (ruler.barSpacing / 2) + - (ruler.barSpacing * barIndex); + (ruler.barSpacing * stackIndex); } }); }; diff --git a/src/scales/scale.linear.js b/src/scales/scale.linear.js index 301af6bc986..319243b23e5 100644 --- a/src/scales/scale.linear.js +++ b/src/scales/scale.linear.js @@ -28,21 +28,43 @@ module.exports = function(Chart) { me.min = null; me.max = null; - if (opts.stacked) { - var valuesPerType = {}; + var hasStacks = opts.stacked; + if (hasStacks === undefined) { + helpers.each(datasets, function(dataset, datasetIndex) { + if (hasStacks) { + return; + } + + var meta = chart.getDatasetMeta(datasetIndex); + if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta) && + meta.stack !== undefined) { + hasStacks = true; + } + }); + } + + if (opts.stacked || hasStacks) { + var valuesPerStack = {}; helpers.each(datasets, function(dataset, datasetIndex) { var meta = chart.getDatasetMeta(datasetIndex); - if (valuesPerType[meta.type] === undefined) { - valuesPerType[meta.type] = { + var key = [ + meta.type, + // we have a separate stack for stack=undefined datasets when the opts.stacked is undefined + ((opts.stacked === undefined && meta.stack === undefined) ? datasetIndex : ''), + meta.stack + ].join('.'); + + if (valuesPerStack[key] === undefined) { + valuesPerStack[key] = { positiveValues: [], negativeValues: [] }; } // Store these per type - var positiveValues = valuesPerType[meta.type].positiveValues; - var negativeValues = valuesPerType[meta.type].negativeValues; + var positiveValues = valuesPerStack[key].positiveValues; + var negativeValues = valuesPerStack[key].negativeValues; if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta)) { helpers.each(dataset.data, function(rawValue, index) { @@ -65,7 +87,7 @@ module.exports = function(Chart) { } }); - helpers.each(valuesPerType, function(valuesForType) { + helpers.each(valuesPerStack, function(valuesForType) { var values = valuesForType.positiveValues.concat(valuesForType.negativeValues); var minVal = helpers.min(values); var maxVal = helpers.max(values); diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js index 160d35f23fd..5d1d329f9ab 100644 --- a/src/scales/scale.logarithmic.js +++ b/src/scales/scale.logarithmic.js @@ -32,18 +32,40 @@ module.exports = function(Chart) { me.max = null; me.minNotZero = null; - if (opts.stacked) { - var valuesPerType = {}; + var hasStacks = opts.stacked; + if (hasStacks === undefined) { + helpers.each(datasets, function(dataset, datasetIndex) { + if (hasStacks) { + return; + } + + var meta = chart.getDatasetMeta(datasetIndex); + if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta) && + meta.stack !== undefined) { + hasStacks = true; + } + }); + } + + if (opts.stacked || hasStacks) { + var valuesPerStack = {}; helpers.each(datasets, function(dataset, datasetIndex) { var meta = chart.getDatasetMeta(datasetIndex); + var key = [ + meta.type, + // we have a separate stack for stack=undefined datasets when the opts.stacked is undefined + ((opts.stacked === undefined && meta.stack === undefined) ? datasetIndex : ''), + meta.stack + ].join('.'); + if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta)) { - if (valuesPerType[meta.type] === undefined) { - valuesPerType[meta.type] = []; + if (valuesPerStack[key] === undefined) { + valuesPerStack[key] = []; } helpers.each(dataset.data, function(rawValue, index) { - var values = valuesPerType[meta.type]; + var values = valuesPerStack[key]; var value = +me.getRightValue(rawValue); if (isNaN(value) || meta.data[index].hidden) { return; @@ -61,7 +83,7 @@ module.exports = function(Chart) { } }); - helpers.each(valuesPerType, function(valuesForType) { + 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); diff --git a/test/controller.bar.tests.js b/test/controller.bar.tests.js index fea88633ce9..38f1b733a1a 100644 --- a/test/controller.bar.tests.js +++ b/test/controller.bar.tests.js @@ -52,41 +52,612 @@ describe('Bar controller tests', function() { expect(meta.yAxisID).toBe('firstYScaleID'); }); - it('should correctly count the number of bar datasets', function() { - var chart = window.acquireChart({ - type: 'bar', - data: { - datasets: [ - {data: [], type: 'line'}, - {data: [], hidden: true}, - {data: []}, - {data: []} - ], - labels: [] - } + it('should correctly count the number of stacks ignoring datasets of other types and hidden datasets', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], type: 'line'}, + {data: [], hidden: true}, + {data: []}, + {data: []} + ], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackCount()).toBe(2); }); + }); - var meta = chart.getDatasetMeta(1); - expect(meta.controller.getBarCount()).toBe(2); + it('should correctly count the number of stacks when a group is not specified', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: []}, + {data: []}, + {data: []}, + {data: []} + ], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackCount()).toBe(4); + }); }); - it('should correctly get the bar index accounting for hidden datasets', function() { - var chart = window.acquireChart({ - type: 'bar', - data: { - datasets: [ - {data: []}, - {data: [], hidden: true}, - {data: [], type: 'line'}, - {data: []} - ], - labels: [] - } + it('should correctly count the number of stacks when a group is not specified and the scale is stacked', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: []}, + {data: []}, + {data: []}, + {data: []} + ], + labels: [] + }, + options: { + scales: { + xAxes: [{ + stacked: true + }], + yAxes: [{ + stacked: true + }] + } + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackCount()).toBe(1); }); + }); - var meta = chart.getDatasetMeta(1); - expect(meta.controller.getBarIndex(0)).toBe(0); - expect(meta.controller.getBarIndex(3)).toBe(1); + it('should correctly count the number of stacks when a group is not specified and the scale is not stacked', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: []}, + {data: []}, + {data: []}, + {data: []} + ], + labels: [] + }, + options: { + scales: { + xAxes: [{ + stacked: false + }], + yAxes: [{ + stacked: false + }] + } + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackCount()).toBe(4); + }); + }); + + it('should correctly count the number of stacks when a group is specified for some', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: []}, + {data: []} + ], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(3); + expect(meta.controller.getStackCount()).toBe(3); + }); + }); + + it('should correctly count the number of stacks when a group is specified for some and the scale is stacked', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: []}, + {data: []} + ], + labels: [] + }, + options: { + scales: { + xAxes: [{ + stacked: true + }], + yAxes: [{ + stacked: true + }] + } + } + }); + + var meta = chart.getDatasetMeta(3); + expect(meta.controller.getStackCount()).toBe(2); + }); + }); + + it('should correctly count the number of stacks when a group is specified for some and the scale is not stacked', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: []}, + {data: []} + ], + labels: [] + }, + options: { + scales: { + xAxes: [{ + stacked: false + }], + yAxes: [{ + stacked: false + }] + } + } + }); + + var meta = chart.getDatasetMeta(3); + expect(meta.controller.getStackCount()).toBe(4); + }); + }); + + it('should correctly count the number of stacks when a group is specified for all', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack2'}, + {data: [], stack: 'stack2'} + ], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(3); + expect(meta.controller.getStackCount()).toBe(2); + }); + }); + + it('should correctly count the number of stacks when a group is specified for all and the scale is stacked', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack2'}, + {data: [], stack: 'stack2'} + ], + labels: [] + }, + options: { + scales: { + xAxes: [{ + stacked: true + }], + yAxes: [{ + stacked: true + }] + } + } + }); + + var meta = chart.getDatasetMeta(3); + expect(meta.controller.getStackCount()).toBe(2); + }); + }); + + it('should correctly count the number of stacks when a group is specified for all and the scale is not stacked', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack2'}, + {data: [], stack: 'stack2'} + ], + labels: [] + }, + options: { + scales: { + xAxes: [{ + stacked: false + }], + yAxes: [{ + stacked: false + }] + } + } + }); + + var meta = chart.getDatasetMeta(3); + expect(meta.controller.getStackCount()).toBe(4); + }); + }); + + it('should correctly get the stack index accounting for datasets of other types and hidden datasets', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: []}, + {data: [], hidden: true}, + {data: [], type: 'line'}, + {data: []} + ], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackIndex(0)).toBe(0); + expect(meta.controller.getStackIndex(3)).toBe(1); + }); + }); + + it('should correctly get the stack index when a group is not specified', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: []}, + {data: []}, + {data: []}, + {data: []} + ], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackIndex(0)).toBe(0); + expect(meta.controller.getStackIndex(1)).toBe(1); + expect(meta.controller.getStackIndex(2)).toBe(2); + expect(meta.controller.getStackIndex(3)).toBe(3); + }); + }); + + it('should correctly get the stack index when a group is not specified and the scale is stacked', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: []}, + {data: []}, + {data: []}, + {data: []} + ], + labels: [] + }, + options: { + scales: { + xAxes: [{ + stacked: true + }], + yAxes: [{ + stacked: true + }] + } + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackIndex(0)).toBe(0); + expect(meta.controller.getStackIndex(1)).toBe(0); + expect(meta.controller.getStackIndex(2)).toBe(0); + expect(meta.controller.getStackIndex(3)).toBe(0); + }); + }); + + it('should correctly get the stack index when a group is not specified and the scale is not stacked', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: []}, + {data: []}, + {data: []}, + {data: []} + ], + labels: [] + }, + options: { + scales: { + xAxes: [{ + stacked: false + }], + yAxes: [{ + stacked: false + }] + } + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackIndex(0)).toBe(0); + expect(meta.controller.getStackIndex(1)).toBe(1); + expect(meta.controller.getStackIndex(2)).toBe(2); + expect(meta.controller.getStackIndex(3)).toBe(3); + }); + }); + + it('should correctly get the stack index when a group is specified for some', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: []}, + {data: []} + ], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackIndex(0)).toBe(0); + expect(meta.controller.getStackIndex(1)).toBe(0); + expect(meta.controller.getStackIndex(2)).toBe(1); + expect(meta.controller.getStackIndex(3)).toBe(2); + }); + }); + + it('should correctly get the stack index when a group is specified for some and the scale is stacked', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: []}, + {data: []} + ], + labels: [] + }, + options: { + scales: { + xAxes: [{ + stacked: true + }], + yAxes: [{ + stacked: true + }] + } + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackIndex(0)).toBe(0); + expect(meta.controller.getStackIndex(1)).toBe(0); + expect(meta.controller.getStackIndex(2)).toBe(1); + expect(meta.controller.getStackIndex(3)).toBe(1); + }); + }); + + it('should correctly get the stack index when a group is specified for some and the scale is not stacked', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: []}, + {data: []} + ], + labels: [] + }, + options: { + scales: { + xAxes: [{ + stacked: false + }], + yAxes: [{ + stacked: false + }] + } + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackIndex(0)).toBe(0); + expect(meta.controller.getStackIndex(1)).toBe(1); + expect(meta.controller.getStackIndex(2)).toBe(2); + expect(meta.controller.getStackIndex(3)).toBe(3); + }); + }); + + it('should correctly get the stack index when a group is specified for all', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack2'}, + {data: [], stack: 'stack2'} + ], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackIndex(0)).toBe(0); + expect(meta.controller.getStackIndex(1)).toBe(0); + expect(meta.controller.getStackIndex(2)).toBe(1); + expect(meta.controller.getStackIndex(3)).toBe(1); + }); + }); + + it('should correctly get the stack index when a group is specified for all and the scale is stacked', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack2'}, + {data: [], stack: 'stack2'} + ], + labels: [] + }, + options: { + scales: { + xAxes: [{ + stacked: true + }], + yAxes: [{ + stacked: true + }] + } + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackIndex(0)).toBe(0); + expect(meta.controller.getStackIndex(1)).toBe(0); + expect(meta.controller.getStackIndex(2)).toBe(1); + expect(meta.controller.getStackIndex(3)).toBe(1); + }); + }); + + it('should correctly get the stack index when a group is specified for all and the scale is not stacked', function() { + [ + 'bar', + 'horizontalBar' + ].forEach(function(barType) { + var chart = window.acquireChart({ + type: barType, + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack2'}, + {data: [], stack: 'stack2'} + ], + labels: [] + }, + options: { + scales: { + xAxes: [{ + stacked: false + }], + yAxes: [{ + stacked: false + }] + } + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller.getStackIndex(0)).toBe(0); + expect(meta.controller.getStackIndex(1)).toBe(1); + expect(meta.controller.getStackIndex(2)).toBe(2); + expect(meta.controller.getStackIndex(3)).toBe(3); + }); }); it('should create rectangle elements for each data item during initialization', function() { @@ -239,9 +810,7 @@ describe('Bar controller tests', function() { options: { scales: { xAxes: [{ - type: 'category', - stacked: true, - barPercentage: 1 + type: 'category' }], yAxes: [{ type: 'linear', @@ -254,10 +823,10 @@ describe('Bar controller tests', function() { var meta0 = chart.getDatasetMeta(0); [ - {b: 290, w: 93, x: 86, y: 161}, - {b: 290, w: 93, x: 202, y: 419}, - {b: 290, w: 93, x: 318, y: 161}, - {b: 290, w: 93, x: 436, y: 419} + {b: 290, w: 83, x: 86, y: 161}, + {b: 290, w: 83, x: 202, y: 419}, + {b: 290, w: 83, x: 318, y: 161}, + {b: 290, w: 83, x: 434, y: 419} ].forEach(function(values, i) { expect(meta0.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta0.data[i]._model.width).toBeCloseToPixel(values.w); @@ -268,10 +837,10 @@ describe('Bar controller tests', function() { var meta1 = chart.getDatasetMeta(1); [ - {b: 161, w: 93, x: 86, y: 32}, - {b: 290, w: 93, x: 202, y: 97}, - {b: 161, w: 93, x: 318, y: 161}, - {b: 419, w: 93, x: 436, y: 471} + {b: 161, w: 83, x: 86, y: 32}, + {b: 290, w: 83, x: 202, y: 97}, + {b: 161, w: 83, x: 318, y: 161}, + {b: 419, w: 83, x: 434, y: 471} ].forEach(function(values, i) { expect(meta1.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta1.data[i]._model.width).toBeCloseToPixel(values.w); @@ -296,9 +865,7 @@ describe('Bar controller tests', function() { options: { scales: { xAxes: [{ - type: 'category', - stacked: true, - barPercentage: 1 + type: 'category' }], yAxes: [{ type: 'linear', @@ -311,10 +878,10 @@ describe('Bar controller tests', function() { var meta0 = chart.getDatasetMeta(0); [ - {b: 290, w: 93, x: 86, y: 161}, - {b: 290, w: 93, x: 202, y: 419}, - {b: 290, w: 93, x: 318, y: 161}, - {b: 290, w: 93, x: 436, y: 419} + {b: 290, w: 83, x: 86, y: 161}, + {b: 290, w: 83, x: 202, y: 419}, + {b: 290, w: 83, x: 318, y: 161}, + {b: 290, w: 83, x: 434, y: 419} ].forEach(function(values, i) { expect(meta0.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta0.data[i]._model.width).toBeCloseToPixel(values.w); @@ -325,10 +892,10 @@ describe('Bar controller tests', function() { var meta1 = chart.getDatasetMeta(1); [ - {b: 161, w: 93, x: 86, y: 32}, - {b: 290, w: 93, x: 202, y: 97}, - {b: 161, w: 93, x: 318, y: 161}, - {b: 419, w: 93, x: 436, y: 471} + {b: 161, w: 83, x: 86, y: 32}, + {b: 290, w: 83, x: 202, y: 97}, + {b: 161, w: 83, x: 318, y: 161}, + {b: 419, w: 83, x: 434, y: 471} ].forEach(function(values, i) { expect(meta1.data[i]._model.base).toBeCloseToPixel(values.b); expect(meta1.data[i]._model.width).toBeCloseToPixel(values.w); @@ -337,6 +904,144 @@ describe('Bar controller tests', function() { }); }); + it('should get the correct bar points for grouped stacked chart if the group name is same', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [10, -10, 10, -10], + label: 'dataset1', + stack: 'stack1' + }, { + data: [10, 15, 0, -4], + label: 'dataset2', + stack: 'stack1' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + scales: { + xAxes: [{ + type: 'category' + }], + yAxes: [{ + type: 'linear', + stacked: true + }] + } + } + }); + + var meta0 = chart.getDatasetMeta(0); + + [ + {b: 290, w: 83, x: 86, y: 161}, + {b: 290, w: 83, x: 202, y: 419}, + {b: 290, w: 83, x: 318, y: 161}, + {b: 290, w: 83, x: 434, y: 419} + ].forEach(function(values, i) { + expect(meta0.data[i]._model.base).toBeCloseToPixel(values.b); + expect(meta0.data[i]._model.width).toBeCloseToPixel(values.w); + expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); + }); + + var meta = chart.getDatasetMeta(1); + + [ + {b: 161, w: 83, x: 86, y: 32}, + {b: 290, w: 83, x: 202, y: 97}, + {b: 161, w: 83, x: 318, y: 161}, + {b: 419, w: 83, x: 434, y: 471} + ].forEach(function(values, i) { + expect(meta.data[i]._model.base).toBeCloseToPixel(values.b); + expect(meta.data[i]._model.width).toBeCloseToPixel(values.w); + expect(meta.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta.data[i]._model.y).toBeCloseToPixel(values.y); + }); + }); + + it('should get the correct bar points for grouped stacked chart if the group name is different', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [1, 2], + stack: 'stack1' + }, { + data: [1, 2], + stack: 'stack2' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + scales: { + xAxes: [{ + type: 'category' + }], + yAxes: [{ + stacked: true, + type: 'linear' + }] + } + } + }); + + var meta = chart.getDatasetMeta(1); + + [ + {x: 108, y: 258}, + {x: 224, y: 32} + ].forEach(function(values, i) { + expect(meta.data[i]._model.base).toBeCloseToPixel(484); + expect(meta.data[i]._model.width).toBeCloseToPixel(40); + expect(meta.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta.data[i]._model.y).toBeCloseToPixel(values.y); + }); + }); + + it('should get the correct bar points for grouped stacked chart', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [1, 2], + stack: 'stack1' + }, { + data: [0.5, 1], + stack: 'stack2' + }, { + data: [0.5, 1], + stack: 'stack2' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + scales: { + xAxes: [{ + type: 'category' + }], + yAxes: [{ + stacked: true, + type: 'linear' + }] + } + } + }); + + var meta = chart.getDatasetMeta(2); + + [ + {b: 371, x: 108, y: 258}, + {b: 258, x: 224, y: 32} + ].forEach(function(values, i) { + expect(meta.data[i]._model.base).toBeCloseToPixel(values.b); + expect(meta.data[i]._model.width).toBeCloseToPixel(40); + expect(meta.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta.data[i]._model.y).toBeCloseToPixel(values.y); + }); + }); + it('should update elements when the scales are stacked and the y axis is logarithmic', function() { var chart = window.acquireChart({ type: 'bar', @@ -564,10 +1269,11 @@ describe('Bar controller tests', function() { var chart = window.acquireChart(this.config); var meta = chart.getDatasetMeta(0); var xScale = chart.scales[meta.xAxisID]; + var yScale = chart.scales[meta.yAxisID]; var categoryPercentage = xScale.options.categoryPercentage; var barPercentage = xScale.options.barPercentage; - var stacked = xScale.options.stacked; + var stacked = yScale.options.stacked; var totalBarWidth = 0; for (var i = 0; i < chart.data.datasets.length; i++) { @@ -613,8 +1319,10 @@ describe('Bar controller tests', function() { ticks: { min: 'March', max: 'May', - }, - stacked: true, + } + }], + yAxes: [{ + stacked: true }] } } @@ -638,11 +1346,12 @@ describe('Bar controller tests', function() { afterEach(function() { var chart = window.acquireChart(this.config); var meta = chart.getDatasetMeta(0); + var xScale = chart.scales[meta.xAxisID]; var yScale = chart.scales[meta.yAxisID]; var categoryPercentage = yScale.options.categoryPercentage; var barPercentage = yScale.options.barPercentage; - var stacked = yScale.options.stacked; + var stacked = xScale.options.stacked; var totalBarHeight = 0; for (var i = 0; i < chart.data.datasets.length; i++) { @@ -684,12 +1393,14 @@ describe('Bar controller tests', function() { data: this.data, options: { scales: { + xAxes: [{ + stacked: true + }], yAxes: [{ ticks: { min: 'March', max: 'May', - }, - stacked: true, + } }] } } From 7c3e71d58b6cb4b169ef03ca22f7bf2cfd09b912 Mon Sep 17 00:00:00 2001 From: Jake Date: Mon, 2 Jan 2017 13:26:51 -0500 Subject: [PATCH 46/61] update copyright date --- gulpfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gulpfile.js b/gulpfile.js index 1ce312d9098..7442cfe6d25 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -29,7 +29,7 @@ var header = "/*!\n" + " * http://chartjs.org/\n" + " * Version: {{ version }}\n" + " *\n" + - " * Copyright 2016 Nick Downie\n" + + " * Copyright 2017 Nick Downie\n" + " * Released under the MIT license\n" + " * https://github.com/chartjs/Chart.js/blob/master/LICENSE.md\n" + " */\n"; From 83c54194aeba03c07210188d4084f123d0df66a7 Mon Sep 17 00:00:00 2001 From: SAiTO TOSHiKi Date: Thu, 5 Jan 2017 22:00:05 +0800 Subject: [PATCH 47/61] Fix : Scale label display at top and right. (#3741) Fix Scale position at rotation when scale is top. --- src/core/core.scale.js | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/core/core.scale.js b/src/core/core.scale.js index 06047e4e39a..b7d04354755 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -338,8 +338,13 @@ module.exports = function(Chart) { // Ensure that our ticks are always inside the canvas. When rotated, ticks are right aligned which means that the right padding is dominated // by the font height - me.paddingLeft = me.labelRotation !== 0 ? (cosRotation * firstLabelWidth) + 3 : firstLabelWidth / 2 + 3; // add 3 px to move away from canvas edges - me.paddingRight = me.labelRotation !== 0 ? (sinRotation * lineSpace) + 3 : lastLabelWidth / 2 + 3; // when rotated + if (me.labelRotation !== 0) { + me.paddingLeft = opts.position === 'bottom'? (cosRotation * firstLabelWidth) + 3: (cosRotation * lineSpace) + 3; // add 3 px to move away from canvas edges + me.paddingRight = opts.position === 'bottom'? (cosRotation * lineSpace) + 3: (cosRotation * lastLabelWidth) + 3; + } else { + me.paddingLeft = firstLabelWidth / 2 + 3; // add 3 px to move away from canvas edges + me.paddingRight = lastLabelWidth / 2 + 3; + } } else { // A vertical axis is more constrained by the width. Labels are the dominant factor here, so get that length first // Account for padding @@ -578,15 +583,21 @@ module.exports = function(Chart) { var textBaseline = 'middle'; if (isHorizontal) { - if (!isRotated) { - textBaseline = options.position === 'top' ? 'bottom' : 'top'; - } - textAlign = isRotated ? 'right' : 'center'; + if (options.position === 'bottom') { + // bottom + textBaseline = !isRotated? 'top':'middle'; + textAlign = !isRotated? 'center': 'right'; + labelY = me.top + tl; + } else { + // top + textBaseline = !isRotated? 'bottom':'middle'; + textAlign = !isRotated? 'center': 'left'; + labelY = me.bottom - tl; + } var xLineValue = me.getPixelForTick(index) + helpers.aliasPixel(lineWidth); // xvalues for grid lines labelX = me.getPixelForTick(index, gridLines.offsetGridLines) + optionTicks.labelOffset; // x values for optionTicks (need to consider offsetLabel option) - labelY = (isRotated) ? me.top + 12 : options.position === 'top' ? me.bottom - tl : me.top + tl; tx1 = tx2 = x1 = x2 = xLineValue; ty1 = yTickStart; From 27b2e332c686e76cfe373a2d5377d53d01a7a84a Mon Sep 17 00:00:00 2001 From: mdewilde Date: Sun, 8 Jan 2017 14:54:03 +0100 Subject: [PATCH 48/61] Correct anchor link (#3772) --- docs/02-Scales.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/02-Scales.md b/docs/02-Scales.md index ae7758326f1..af7812ce03b 100644 --- a/docs/02-Scales.md +++ b/docs/02-Scales.md @@ -36,7 +36,7 @@ afterFit | Function | undefined | Callback that runs after the scale fits to the afterUpdate | Function | undefined | Callback that runs at the end of the update process. Passed a single argument, the scale instance. **gridLines** | Object | - | See [grid line configuration](#grid-line-configuration) section. **scaleLabel** | Object | | See [scale title configuration](#scale-title-configuration) section. -**ticks** | Object | | See [ticks configuration](#ticks-configuration) section. +**ticks** | Object | | See [tick configuration](#tick-configuration) section. #### Grid Line Configuration From 9e9b9cf46d452dfbfb2fa19a9cd362d4405abfdc Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Fri, 6 Jan 2017 21:41:10 -0500 Subject: [PATCH 49/61] when the cutoutPercentage is 0, the inner radius should be 0 --- src/controllers/controller.doughnut.js | 2 +- test/controller.doughnut.tests.js | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/controllers/controller.doughnut.js b/src/controllers/controller.doughnut.js index 3dcd33e9f63..70ee22e1841 100644 --- a/src/controllers/controller.doughnut.js +++ b/src/controllers/controller.doughnut.js @@ -183,7 +183,7 @@ module.exports = function(Chart) { chart.borderWidth = me.getMaxBorderWidth(meta.data); chart.outerRadius = Math.max((minSize - chart.borderWidth) / 2, 0); - chart.innerRadius = Math.max(cutoutPercentage ? (chart.outerRadius / 100) * (cutoutPercentage) : 1, 0); + chart.innerRadius = Math.max(cutoutPercentage ? (chart.outerRadius / 100) * (cutoutPercentage) : 0, 0); chart.radiusLength = (chart.outerRadius - chart.innerRadius) / chart.getVisibleDatasetCount(); chart.offsetX = offset.x * chart.outerRadius; chart.offsetY = offset.y * chart.outerRadius; diff --git a/test/controller.doughnut.tests.js b/test/controller.doughnut.tests.js index d9319bb807a..f4c0c959e30 100644 --- a/test/controller.doughnut.tests.js +++ b/test/controller.doughnut.tests.js @@ -40,6 +40,20 @@ describe('Doughnut controller tests', function() { expect(meta.data[3] instanceof Chart.elements.Arc).toBe(true); }); + it('should set the innerRadius to 0 if the config option is 0', function() { + var chart = window.acquireChart({ + type: 'pie', + data: { + datasets: [{ + data: [10, 15, 0, 4] + }], + labels: [] + } + }); + + expect(chart.innerRadius).toBe(0); + }); + it ('should reset and update elements', function() { var chart = window.acquireChart({ type: 'doughnut', From 312773ba7b7e2acf6cd1273c92017d3c197a992b Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sat, 14 Jan 2017 14:38:56 +0100 Subject: [PATCH 50/61] Platform event API abstraction Move base platform definition and logic in src/platform/platform.js and simplify the browser -> Chart.js event mapping by listing only different naming then fallback to the native type. Replace `createEvent` by `add/removeEventListener` methods which dispatch Chart.js IEvent objects instead of native events. Move `add/removeResizeListener` implementation into the DOM platform which is now accessible via `platform.add/removeEventListener(chart, 'resize', listener)`. Finally, remove `bindEvent` and `unbindEvent` from the helpers since the implementation is specific to the chart controller (and should be private). --- src/chart.js | 4 +- src/core/core.controller.js | 86 ++++++++----- src/core/core.helpers.js | 84 ------------- src/core/core.interaction.js | 6 +- src/core/core.legend.js | 2 +- src/core/core.tooltip.js | 2 +- src/platforms/platform.dom.js | 220 ++++++++++++++++++++-------------- src/platforms/platform.js | 69 +++++++++++ test/platform.dom.tests.js | 4 - 9 files changed, 263 insertions(+), 214 deletions(-) create mode 100644 src/platforms/platform.js diff --git a/src/chart.js b/src/chart.js index 186d07a42f5..77d2bb636c8 100644 --- a/src/chart.js +++ b/src/chart.js @@ -4,6 +4,7 @@ var Chart = require('./core/core.js')(); require('./core/core.helpers')(Chart); +require('./platforms/platform.js')(Chart); require('./core/core.canvasHelpers')(Chart); require('./core/core.plugin.js')(Chart); require('./core/core.element')(Chart); @@ -19,9 +20,6 @@ require('./core/core.legend')(Chart); require('./core/core.interaction')(Chart); require('./core/core.tooltip')(Chart); -// By default, we only load the browser platform. -Chart.platform = require('./platforms/platform.dom')(Chart); - require('./elements/element.arc')(Chart); require('./elements/element.line')(Chart); require('./elements/element.point')(Chart); diff --git a/src/core/core.controller.js b/src/core/core.controller.js index e64cb3a9dde..616df547be7 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -3,6 +3,7 @@ module.exports = function(Chart) { var helpers = Chart.helpers; + var platform = Chart.platform; // Create a dictionary of chart types, to allow for extension of existing types Chart.types = {}; @@ -63,7 +64,7 @@ module.exports = function(Chart) { config = initConfig(config); - var context = Chart.platform.acquireContext(item, config); + var context = platform.acquireContext(item, config); var canvas = context && context.canvas; var height = canvas && canvas.height; var width = canvas && canvas.width; @@ -99,21 +100,6 @@ module.exports = function(Chart) { return me; } - helpers.retinaScale(instance); - - // Responsiveness is currently based on the use of an iframe, however this method causes - // performance issues and could be troublesome when used with ad blockers. So make sure - // that the user is still able to create a chart without iframe when responsive is false. - // See https://github.com/chartjs/Chart.js/issues/2210 - if (me.options.responsive) { - helpers.addResizeListener(canvas.parentNode, function() { - me.resize(); - }); - - // Initial resize before chart draws (must be silent to preserve initial animations). - me.resize(true); - } - me.initialize(); return me; @@ -126,8 +112,15 @@ module.exports = function(Chart) { // Before init plugin notification Chart.plugins.notify(me, 'beforeInit'); + helpers.retinaScale(me.chart); + me.bindEvents(); + if (me.options.responsive) { + // Initial resize before chart draws (must be silent to preserve initial animations). + me.resize(true); + } + // Make sure controllers are built first so that each dataset is bound to an axis before the scales // are built me.ensureScalesHaveIDs(); @@ -559,10 +552,9 @@ module.exports = function(Chart) { } if (canvas) { - helpers.unbindEvents(me, me.events); - helpers.removeResizeListener(canvas.parentNode); + me.unbindEvents(); helpers.clear(me.chart); - Chart.platform.releaseContext(me.chart.ctx); + platform.releaseContext(me.chart.ctx); me.chart.canvas = null; me.chart.ctx = null; } @@ -587,10 +579,48 @@ module.exports = function(Chart) { me.tooltip.initialize(); }, + /** + * @private + */ bindEvents: function() { var me = this; - helpers.bindEvents(me, me.options.events, function(evt) { - me.eventHandler(evt); + var listeners = me._listeners = {}; + var listener = function() { + me.eventHandler.apply(me, arguments); + }; + + helpers.each(me.options.events, function(type) { + platform.addEventListener(me, type, listener); + listeners[type] = listener; + }); + + // Responsiveness is currently based on the use of an iframe, however this method causes + // performance issues and could be troublesome when used with ad blockers. So make sure + // that the user is still able to create a chart without iframe when responsive is false. + // See https://github.com/chartjs/Chart.js/issues/2210 + if (me.options.responsive) { + listener = function() { + me.resize(); + }; + + platform.addEventListener(me, 'resize', listener); + listeners.resize = listener; + } + }, + + /** + * @private + */ + unbindEvents: function() { + var me = this; + var listeners = me._listeners; + if (!listeners) { + return; + } + + delete me._listeners; + helpers.each(listeners, function(listener, type) { + platform.removeEventListener(me, type, listener); }); }, @@ -606,6 +636,9 @@ module.exports = function(Chart) { } }, + /** + * @private + */ eventHandler: function(e) { var me = this; var tooltip = me.tooltip; @@ -615,12 +648,9 @@ module.exports = function(Chart) { me._bufferedRender = true; me._bufferedRequest = null; - // Create platform agnostic chart event using platform specific code - var chartEvent = Chart.platform.createEvent(e, me.chart); - - var changed = me.handleEvent(chartEvent); - changed |= tooltip && tooltip.handleEvent(chartEvent); - changed |= Chart.plugins.notify(me, 'onEvent', [chartEvent]); + var changed = me.handleEvent(e); + changed |= tooltip && tooltip.handleEvent(e); + changed |= Chart.plugins.notify(me, 'onEvent', [e]); var bufferedRequest = me._bufferedRequest; if (bufferedRequest) { @@ -644,7 +674,7 @@ module.exports = function(Chart) { /** * Handle an event * @private - * param e {Core.Event} the event to handle + * @param {IEvent} event the event to handle * @return {Boolean} true if the chart needs to re-render */ handleEvent: function(e) { diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index bfbcf23d5f5..f95af269f6b 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -734,23 +734,6 @@ module.exports = function(Chart) { node['on' + eventType] = helpers.noop; } }; - helpers.bindEvents = function(chartInstance, arrayOfEvents, handler) { - // Create the events object if it's not already present - var events = chartInstance.events = chartInstance.events || {}; - - helpers.each(arrayOfEvents, function(eventName) { - events[eventName] = function() { - handler.apply(chartInstance, arguments); - }; - helpers.addEvent(chartInstance.chart.canvas, eventName, events[eventName]); - }); - }; - helpers.unbindEvents = function(chartInstance, arrayOfEvents) { - var canvas = chartInstance.chart.canvas; - helpers.each(arrayOfEvents, function(handler, eventName) { - helpers.removeEvent(canvas, eventName, handler); - }); - }; // Private helper function to convert max-width/max-height values that may be percentages into a number function parseMaxStyle(styleValue, node, parentProperty) { @@ -941,73 +924,6 @@ module.exports = function(Chart) { return color(c); }; - helpers.addResizeListener = function(node, callback) { - var iframe = document.createElement('iframe'); - iframe.className = 'chartjs-hidden-iframe'; - iframe.style.cssText = - 'display:block;'+ - 'overflow:hidden;'+ - 'border:0;'+ - 'margin:0;'+ - 'top:0;'+ - 'left:0;'+ - 'bottom:0;'+ - 'right:0;'+ - 'height:100%;'+ - 'width:100%;'+ - 'position:absolute;'+ - 'pointer-events:none;'+ - 'z-index:-1;'; - - // Prevent the iframe to gain focus on tab. - // https://github.com/chartjs/Chart.js/issues/3090 - iframe.tabIndex = -1; - - // Let's keep track of this added iframe and thus avoid DOM query when removing it. - var stub = node._chartjs = { - resizer: iframe, - ticking: false - }; - - // Throttle the callback notification until the next animation frame. - var notify = function() { - if (!stub.ticking) { - stub.ticking = true; - helpers.requestAnimFrame.call(window, function() { - if (stub.resizer) { - stub.ticking = false; - return callback(); - } - }); - } - }; - - // If the iframe is re-attached to the DOM, the resize listener is removed because the - // content is reloaded, so make sure to install the handler after the iframe is loaded. - // https://github.com/chartjs/Chart.js/issues/3521 - helpers.addEvent(iframe, 'load', function() { - helpers.addEvent(iframe.contentWindow || iframe, 'resize', notify); - - // The iframe size might have changed while loading, which can also - // happen if the size has been changed while detached from the DOM. - notify(); - }); - - node.insertBefore(iframe, node.firstChild); - }; - helpers.removeResizeListener = function(node) { - if (!node || !node._chartjs) { - return; - } - - var iframe = node._chartjs.resizer; - if (iframe) { - iframe.parentNode.removeChild(iframe); - node._chartjs.resizer = null; - } - - delete node._chartjs; - }; helpers.isArray = Array.isArray? function(obj) { return Array.isArray(obj); diff --git a/src/core/core.interaction.js b/src/core/core.interaction.js index 0888fa9591b..03423c0d604 100644 --- a/src/core/core.interaction.js +++ b/src/core/core.interaction.js @@ -5,8 +5,8 @@ module.exports = function(Chart) { /** * Helper function to get relative position for an event - * @param e {Event|Core.Event} the event to get the position for - * @param chart {chart} the chart + * @param {Event|IEvent} event - The event to get the position for + * @param {Chart} chart - The chart * @returns {Point} the event position */ function getRelativePosition(e, chart) { @@ -135,8 +135,8 @@ module.exports = function(Chart) { */ /** - * @namespace Chart.Interaction * Contains interaction related functions + * @namespace Chart.Interaction */ Chart.Interaction = { // Helper function for different modes diff --git a/src/core/core.legend.js b/src/core/core.legend.js index 45f51d05923..b80bdbe25ad 100644 --- a/src/core/core.legend.js +++ b/src/core/core.legend.js @@ -439,7 +439,7 @@ module.exports = function(Chart) { /** * Handle an event * @private - * @param e {Core.Event} the event to handle + * @param {IEvent} event - The event to handle * @return {Boolean} true if a change occured */ handleEvent: function(e) { diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index c1ac7830739..32589b652e6 100755 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -763,7 +763,7 @@ module.exports = function(Chart) { /** * Handle an event * @private - * @param e {Core.Event} the event to handle + * @param {IEvent} event - The event to handle * @returns {Boolean} true if the tooltip changed */ handleEvent: function(e) { diff --git a/src/platforms/platform.dom.js b/src/platforms/platform.dom.js index fb41f88259f..abfb3dee3e1 100644 --- a/src/platforms/platform.dom.js +++ b/src/platforms/platform.dom.js @@ -1,57 +1,13 @@ 'use strict'; -/** - * @interface IPlatform - * Allows abstracting platform dependencies away from the chart - */ -/** - * Creates a chart.js event from a platform specific event - * @method IPlatform#createEvent - * @param e {Event} : the platform event to translate - * @returns {Core.Event} chart.js event - */ -/** - * @method IPlatform#acquireContext - * @param item {Object} the context or canvas to use - * @param config {ChartOptions} the chart options - * @returns {CanvasRenderingContext2D} a context2d instance implementing the w3c Canvas 2D context API standard. - */ -/** - * @method IPlatform#releaseContext - * @param context {CanvasRenderingContext2D} the context to release. This is the item returned by @see {@link IPlatform#acquireContext} - */ - // Chart.Platform implementation for targeting a web browser module.exports = function(Chart) { var helpers = Chart.helpers; - /* - * Key is the browser event type - * Chart.js internal events are: - * mouseenter - * mousedown - * mousemove - * mouseup - * mouseout - * click - * dblclick - * contextmenu - * keydown - * keypress - * keyup - */ - var typeMap = { - // Mouse events - mouseenter: 'mouseenter', - mousedown: 'mousedown', - mousemove: 'mousemove', - mouseup: 'mouseup', - mouseout: 'mouseout', - mouseleave: 'mouseout', - click: 'click', - dblclick: 'dblclick', - contextmenu: 'contextmenu', - + // DOM event types -> Chart.js event types. + // Note: only events with different types are mapped. + // https://developer.mozilla.org/en-US/docs/Web/Events + var eventTypeMap = { // Touch events touchstart: 'mousedown', touchmove: 'mousemove', @@ -63,12 +19,7 @@ module.exports = function(Chart) { pointermove: 'mousemove', pointerup: 'mouseup', pointerleave: 'mouseout', - pointerout: 'mouseout', - - // Key events - keydown: 'keydown', - keypress: 'keypress', - keyup: 'keyup', + pointerout: 'mouseout' }; /** @@ -141,38 +92,97 @@ module.exports = function(Chart) { return canvas; } - return { - /** - * Creates a Chart.js event from a raw event - * @method BrowserPlatform#createEvent - * @implements IPlatform.createEvent - * @param e {Event} the raw event (such as a mouse event) - * @param chart {Chart} the chart to use - * @returns {Core.Event} the chart.js event for this event - */ - createEvent: function(e, chart) { - var relativePosition = helpers.getRelativePosition(e, chart); - return { - // allow access to the native event - native: e, - - // our interal event type - type: typeMap[e.type], - - // width and height of chart - width: chart.width, - height: chart.height, - - // Position relative to the canvas - x: relativePosition.x, - y: relativePosition.y - }; - }, + function createEvent(type, chart, x, y, native) { + return { + type: type, + chart: chart, + native: native || null, + x: x !== undefined? x : null, + y: y !== undefined? y : null, + }; + } + + function fromNativeEvent(event, chart) { + var type = eventTypeMap[event.type] || event.type; + var pos = helpers.getRelativePosition(event, chart); + return createEvent(type, chart, pos.x, pos.y, event); + } + + function createResizer(handler) { + var iframe = document.createElement('iframe'); + iframe.className = 'chartjs-hidden-iframe'; + iframe.style.cssText = + 'display:block;'+ + 'overflow:hidden;'+ + 'border:0;'+ + 'margin:0;'+ + 'top:0;'+ + 'left:0;'+ + 'bottom:0;'+ + 'right:0;'+ + 'height:100%;'+ + 'width:100%;'+ + 'position:absolute;'+ + 'pointer-events:none;'+ + 'z-index:-1;'; - /** - * @method BrowserPlatform#acquireContext - * @implements IPlatform#acquireContext - */ + // Prevent the iframe to gain focus on tab. + // https://github.com/chartjs/Chart.js/issues/3090 + iframe.tabIndex = -1; + + // If the iframe is re-attached to the DOM, the resize listener is removed because the + // content is reloaded, so make sure to install the handler after the iframe is loaded. + // https://github.com/chartjs/Chart.js/issues/3521 + helpers.addEvent(iframe, 'load', function() { + helpers.addEvent(iframe.contentWindow || iframe, 'resize', handler); + + // The iframe size might have changed while loading, which can also + // happen if the size has been changed while detached from the DOM. + handler(); + }); + + return iframe; + } + + function addResizeListener(node, listener, chart) { + var stub = node._chartjs = { + ticking: false + }; + + // Throttle the callback notification until the next animation frame. + var notify = function() { + if (!stub.ticking) { + stub.ticking = true; + helpers.requestAnimFrame.call(window, function() { + if (stub.resizer) { + stub.ticking = false; + return listener(createEvent('resize', chart)); + } + }); + } + }; + + // Let's keep track of this added iframe and thus avoid DOM query when removing it. + stub.resizer = createResizer(notify); + + node.insertBefore(stub.resizer, node.firstChild); + } + + function removeResizeListener(node) { + if (!node || !node._chartjs) { + return; + } + + var resizer = node._chartjs.resizer; + if (resizer) { + resizer.parentNode.removeChild(resizer); + node._chartjs.resizer = null; + } + + delete node._chartjs; + } + + return { acquireContext: function(item, config) { if (typeof item === 'string') { item = document.getElementById(item); @@ -200,11 +210,6 @@ module.exports = function(Chart) { return null; }, - /** - * Restores the canvas initial state, such as render/display sizes and style. - * @method BrowserPlatform#releaseContext - * @implements IPlatform#releaseContext - */ releaseContext: function(context) { var canvas = context.canvas; if (!canvas._chartjs) { @@ -232,6 +237,41 @@ module.exports = function(Chart) { canvas.width = canvas.width; delete canvas._chartjs; + }, + + addEventListener: function(chart, type, listener) { + var canvas = chart.chart.canvas; + if (type === 'resize') { + // Note: the resize event is not supported on all browsers. + addResizeListener(canvas.parentNode, listener, chart.chart); + return; + } + + var stub = listener._chartjs || (listener._chartjs = {}); + var proxies = stub.proxies || (stub.proxies = {}); + var proxy = proxies[chart.id + '_' + type] = function(event) { + listener(fromNativeEvent(event, chart.chart)); + }; + + helpers.addEvent(canvas, type, proxy); + }, + + removeEventListener: function(chart, type, listener) { + var canvas = chart.chart.canvas; + if (type === 'resize') { + // Note: the resize event is not supported on all browsers. + removeResizeListener(canvas.parentNode, listener); + return; + } + + var stub = listener._chartjs || {}; + var proxies = stub.proxies || {}; + var proxy = proxies[chart.id + '_' + type]; + if (!proxy) { + return; + } + + helpers.removeEvent(canvas, type, proxy); } }; }; diff --git a/src/platforms/platform.js b/src/platforms/platform.js new file mode 100644 index 00000000000..0f27e5868a6 --- /dev/null +++ b/src/platforms/platform.js @@ -0,0 +1,69 @@ +'use strict'; + +// By default, select the browser (DOM) platform. +// @TODO Make possible to select another platform at build time. +var implementation = require('./platform.dom.js'); + +module.exports = function(Chart) { + /** + * @namespace Chart.platform + * @see https://chartjs.gitbooks.io/proposals/content/Platform.html + * @since 2.4.0 + */ + Chart.platform = { + /** + * Called at chart construction time, returns a context2d instance implementing + * the [W3C Canvas 2D Context API standard]{@link https://www.w3.org/TR/2dcontext/}. + * @param {*} item - The native item from which to acquire context (platform specific) + * @param {Object} options - The chart options + * @returns {CanvasRenderingContext2D} context2d instance + */ + acquireContext: function() {}, + + /** + * Called at chart destruction time, releases any resources associated to the context + * previously returned by the acquireContext() method. + * @param {CanvasRenderingContext2D} context - The context2d instance + * @returns {Boolean} true if the method succeeded, else false + */ + releaseContext: function() {}, + + /** + * Registers the specified listener on the given chart. + * @param {Chart} chart - Chart from which to listen for event + * @param {String} type - The ({@link IEvent}) type to listen for + * @param {Function} listener - Receives a notification (an object that implements + * the {@link IEvent} interface) when an event of the specified type occurs. + */ + addEventListener: function() {}, + + /** + * Removes the specified listener previously registered with addEventListener. + * @param {Chart} chart -Chart from which to remove the listener + * @param {String} type - The ({@link IEvent}) type to remove + * @param {Function} listener - The listener function to remove from the event target. + */ + removeEventListener: function() {} + }; + + /** + * @interface IPlatform + * Allows abstracting platform dependencies away from the chart + * @borrows Chart.platform.acquireContext as acquireContext + * @borrows Chart.platform.releaseContext as releaseContext + * @borrows Chart.platform.addEventListener as addEventListener + * @borrows Chart.platform.removeEventListener as removeEventListener + */ + + /** + * @interface IEvent + * @prop {String} type - The event type name, possible values are: + * 'contextmenu', 'mouseenter', 'mousedown', 'mousemove', 'mouseup', 'mouseout', + * 'click', 'dblclick', 'keydown', 'keypress', 'keyup' and 'resize' + * @prop {*} native - The original native event (null for emulated events, e.g. 'resize') + * @prop {Number} x - The mouse x position, relative to the canvas (null for incompatible events) + * @prop {Number} y - The mouse y position, relative to the canvas (null for incompatible events) + */ + + Chart.helpers.extend(Chart.platform, implementation(Chart)); +}; diff --git a/test/platform.dom.tests.js b/test/platform.dom.tests.js index f20b6e1ff68..19c5bbe1aab 100644 --- a/test/platform.dom.tests.js +++ b/test/platform.dom.tests.js @@ -361,10 +361,6 @@ describe('Platform.dom', function() { // Is type correctly translated expect(notifiedEvent.type).toBe(evt.type); - // Canvas width and height - expect(notifiedEvent.width).toBe(chart.chart.width); - expect(notifiedEvent.height).toBe(chart.chart.height); - // Relative Position expect(notifiedEvent.x).toBe(chart.chart.width / 2); expect(notifiedEvent.y).toBe(chart.chart.height / 2); From c20e57bc8ae86f2881b1f894b1767d95194bada1 Mon Sep 17 00:00:00 2001 From: Thomas Redston Date: Fri, 13 Jan 2017 17:55:28 +0000 Subject: [PATCH 51/61] Only generate ticks we care about Instead of cloning `me.scaleSizeInUnits` moments and probably throwing the vast majority away, only clone what we need. --- src/scales/scale.time.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 61796c6ebcc..9a3e31e63b3 100755 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -324,7 +324,7 @@ module.exports = function(Chart) { me.ticks.push(me.firstTick.clone()); // For every unit in between the first and last moment, create a moment and add it to the ticks tick - for (var i = 1; i <= me.scaleSizeInUnits; ++i) { + for (var i = me.unitScale; i <= me.scaleSizeInUnits; i += me.unitScale) { var newTick = roundedStart.clone().add(i, me.tickUnit); // Are we greater than the max time @@ -332,9 +332,7 @@ module.exports = function(Chart) { break; } - if (i % me.unitScale === 0) { - me.ticks.push(newTick); - } + me.ticks.push(newTick); } // Always show the right tick From ceec907bee33d39da68cb7af111400622aefee23 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sat, 21 Jan 2017 13:12:12 +0100 Subject: [PATCH 52/61] Ignore .gitignore (and more) from Bower packages --- gulpfile.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/gulpfile.js b/gulpfile.js index 7442cfe6d25..6cd6efa5d91 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -71,7 +71,15 @@ function bowerTask() { homepage: package.homepage, license: package.license, version: package.version, - main: outDir + "Chart.js" + main: outDir + "Chart.js", + ignore: [ + '.github', + '.codeclimate.yml', + '.gitignore', + '.npmignore', + '.travis.yml', + 'scripts' + ] }, null, 2); return file('bower.json', json, { src: true }) From 696f8d3a391ca8db6cbd1a1291cf8f3170fc0f1c Mon Sep 17 00:00:00 2001 From: Jerry Chang Date: Sat, 21 Jan 2017 16:42:21 -0800 Subject: [PATCH 53/61] Documentation update on requiring Chart.js using CommonJS and es6 (#3788) --- docs/00-Getting-Started.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/00-Getting-Started.md b/docs/00-Getting-Started.md index b7eb09dfc08..81e03c0a986 100644 --- a/docs/00-Getting-Started.md +++ b/docs/00-Getting-Started.md @@ -44,11 +44,11 @@ To import Chart.js using an awesome module loader: ```javascript // Using CommonJS -var Chart = require('src/chart.js') +var Chart = require('chart.js') var myChart = new Chart({...}) // ES6 -import Chart from 'src/chart.js' +import Chart from 'chart.js' let myChart = new Chart({...}) // Using requirejs From 1934358663ad16773082a44974da91f0be78b4e3 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sun, 22 Jan 2017 13:23:42 -0500 Subject: [PATCH 54/61] remove unnecessary extra init steps --- src/core/core.controller.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 616df547be7..1d2366152ec 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -121,13 +121,9 @@ module.exports = function(Chart) { me.resize(true); } - // Make sure controllers are built first so that each dataset is bound to an axis before the scales - // are built + // Make sure scales have IDs and are built before we build any controllers. me.ensureScalesHaveIDs(); - me.buildOrUpdateControllers(); me.buildScales(); - me.updateLayout(); - me.resetElements(); me.initToolTip(); me.update(); @@ -256,10 +252,6 @@ module.exports = function(Chart) { Chart.scaleService.addScalesToLayout(this); }, - updateLayout: function() { - Chart.layoutService.update(this, this.chart.width, this.chart.height); - }, - buildOrUpdateControllers: function() { var me = this; var types = []; From 1ef9fbf7a65763c13fa4bdf42bf4c68da852b1db Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sun, 22 Jan 2017 12:46:27 -0500 Subject: [PATCH 55/61] inner radius could be slightly negative due to numerical errors --- src/controllers/controller.doughnut.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/controller.doughnut.js b/src/controllers/controller.doughnut.js index 70ee22e1841..3d478058279 100644 --- a/src/controllers/controller.doughnut.js +++ b/src/controllers/controller.doughnut.js @@ -191,7 +191,7 @@ module.exports = function(Chart) { meta.total = me.calculateTotal(); me.outerRadius = chart.outerRadius - (chart.radiusLength * me.getRingIndex(me.index)); - me.innerRadius = me.outerRadius - chart.radiusLength; + me.innerRadius = Math.max(me.outerRadius - chart.radiusLength, 0); helpers.each(meta.data, function(arc, index) { me.updateElement(arc, index, reset); From c6fa4e55822e20b2bdd31d39bc18e19d53cd8966 Mon Sep 17 00:00:00 2001 From: Jakub Juszczak Date: Fri, 27 Jan 2017 13:51:30 +0100 Subject: [PATCH 56/61] =?UTF-8?q?=F0=9F=93=9D=20Add=20vue-chartjs=20to=20d?= =?UTF-8?q?ocs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit vue-chartjs is a wrapper written in vue for chartjs. --- docs/10-Notes.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/10-Notes.md b/docs/10-Notes.md index c6ad42910a1..e292a374046 100644 --- a/docs/10-Notes.md +++ b/docs/10-Notes.md @@ -109,3 +109,6 @@ There are many extensions which are available for use with popular frameworks. S #### Laravel - laravel-chartjs + +#### Vue.js + - vue-chartjs From 979341ecb094d9c6a95de8a47e7836f01587e7d2 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sun, 22 Jan 2017 20:13:40 +0100 Subject: [PATCH 57/61] Plugin hooks and jsdoc enhancements Make all `before` hooks cancellable (except `beforeInit`), meaning that if any plugin return explicitly `false`, the current action is not performed. Ensure that `init` hooks are called before `update` hooks and add associated calling order unit tests. Deprecate `Chart.PluginBase` in favor of `IPlugin` (no more an inheritable class) and document plugin hooks (also rename `extension` by `hook`). --- docs/09-Advanced.md | 2 +- src/core/core.controller.js | 140 +++++++++++++++------------ src/core/core.plugin.js | 174 +++++++++++++++++++++++++++------- test/core.controller.tests.js | 71 ++++++++++++++ 4 files changed, 296 insertions(+), 91 deletions(-) diff --git a/docs/09-Advanced.md b/docs/09-Advanced.md index 4c677c904d4..d3d69d9afc9 100644 --- a/docs/09-Advanced.md +++ b/docs/09-Advanced.md @@ -412,7 +412,7 @@ Plugins will be called at the following times * Before an animation is started * When an event occurs on the canvas (mousemove, click, etc). This requires the `options.events` property handled -Plugins should derive from Chart.PluginBase and implement the following interface +Plugins should implement the `IPlugin` interface: ```javascript { beforeInit: function(chartInstance) { }, diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 1d2366152ec..e6c19659387 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -3,6 +3,7 @@ module.exports = function(Chart) { var helpers = Chart.helpers; + var plugins = Chart.plugins; var platform = Chart.platform; // Create a dictionary of chart types, to allow for extension of existing types @@ -101,16 +102,17 @@ module.exports = function(Chart) { } me.initialize(); + me.update(); return me; }; - helpers.extend(Chart.Controller.prototype, /** @lends Chart.Controller */ { + helpers.extend(Chart.Controller.prototype, /** @lends Chart.Controller.prototype */ { initialize: function() { var me = this; // Before init plugin notification - Chart.plugins.notify(me, 'beforeInit'); + plugins.notify(me, 'beforeInit'); helpers.retinaScale(me.chart); @@ -125,10 +127,9 @@ module.exports = function(Chart) { me.ensureScalesHaveIDs(); me.buildScales(); me.initToolTip(); - me.update(); // After init plugin notification - Chart.plugins.notify(me, 'afterInit'); + plugins.notify(me, 'afterInit'); return me; }, @@ -170,7 +171,7 @@ module.exports = function(Chart) { if (!silent) { // Notify any plugins about the resize var newSize = {width: newWidth, height: newHeight}; - Chart.plugins.notify(me, 'resize', [newSize]); + plugins.notify(me, 'resize', [newSize]); // Notify of resize if (me.options.onResize) { @@ -287,7 +288,6 @@ module.exports = function(Chart) { /** * Reset the elements of all datasets - * @method resetElements * @private */ resetElements: function() { @@ -299,19 +299,20 @@ module.exports = function(Chart) { /** * Resets the chart back to it's state before the initial animation - * @method reset */ reset: function() { this.resetElements(); this.tooltip.initialize(); }, - update: function(animationDuration, lazy) { var me = this; updateConfig(me); - Chart.plugins.notify(me, 'beforeUpdate'); + + if (plugins.notify(me, 'beforeUpdate') === false) { + return; + } // In case the entire data object changed me.tooltip._data = me.data; @@ -324,10 +325,7 @@ module.exports = function(Chart) { me.getDatasetMeta(datasetIndex).controller.buildOrUpdateElements(); }, me); - Chart.layoutService.update(me, me.chart.width, me.chart.height); - - // Apply changes to the datasets that require the scales to have been calculated i.e BorderColor changes - Chart.plugins.notify(me, 'afterScaleUpdate'); + me.updateLayout(); // Can only reset the new controllers after the scales have been updated helpers.each(newControllers, function(controller) { @@ -337,7 +335,7 @@ module.exports = function(Chart) { me.updateDatasets(); // Do this before render so that any plugins that need final scale updates can use it - Chart.plugins.notify(me, 'afterUpdate'); + plugins.notify(me, 'afterUpdate'); if (me._bufferedRender) { me._bufferedRequest = { @@ -350,51 +348,64 @@ module.exports = function(Chart) { }, /** - * @method beforeDatasetsUpdate - * @description Called before all datasets are updated. If a plugin returns false, - * the datasets update will be cancelled until another chart update is triggered. - * @param {Object} instance the chart instance being updated. - * @returns {Boolean} false to cancel the datasets update. - * @memberof Chart.PluginBase - * @since version 2.1.5 - * @instance + * Updates the chart layout unless a plugin returns `false` to the `beforeLayout` + * hook, in which case, plugins will not be called on `afterLayout`. + * @private */ + updateLayout: function() { + var me = this; - /** - * @method afterDatasetsUpdate - * @description Called after all datasets have been updated. Note that this - * extension will not be called if the datasets update has been cancelled. - * @param {Object} instance the chart instance being updated. - * @memberof Chart.PluginBase - * @since version 2.1.5 - * @instance - */ + if (plugins.notify(me, 'beforeLayout') === false) { + return; + } + + Chart.layoutService.update(this, this.chart.width, this.chart.height); + + /** + * Provided for backward compatibility, use `afterLayout` instead. + * @method IPlugin#afterScaleUpdate + * @deprecated since version 2.5.0 + * @todo remove at version 3 + */ + plugins.notify(me, 'afterScaleUpdate'); + plugins.notify(me, 'afterLayout'); + }, /** - * Updates all datasets unless a plugin returns false to the beforeDatasetsUpdate - * extension, in which case no datasets will be updated and the afterDatasetsUpdate - * notification will be skipped. - * @protected - * @instance + * Updates all datasets unless a plugin returns `false` to the `beforeDatasetsUpdate` + * hook, in which case, plugins will not be called on `afterDatasetsUpdate`. + * @private */ updateDatasets: function() { var me = this; - var i, ilen; - if (Chart.plugins.notify(me, 'beforeDatasetsUpdate')) { - for (i = 0, ilen = me.data.datasets.length; i < ilen; ++i) { - me.getDatasetMeta(i).controller.update(); - } + if (plugins.notify(me, 'beforeDatasetsUpdate') === false) { + return; + } - Chart.plugins.notify(me, 'afterDatasetsUpdate'); + for (var i = 0, ilen = me.data.datasets.length; i < ilen; ++i) { + me.getDatasetMeta(i).controller.update(); } + + plugins.notify(me, 'afterDatasetsUpdate'); }, render: function(duration, lazy) { var me = this; - Chart.plugins.notify(me, 'beforeRender'); + + if (plugins.notify(me, 'beforeRender') === false) { + return; + } var animationOptions = me.options.animation; + var onComplete = function() { + plugins.notify(me, 'afterRender'); + var callback = animationOptions && animationOptions.onComplete; + if (callback && callback.call) { + callback.call(me); + } + }; + if (animationOptions && ((typeof duration !== 'undefined' && duration !== 0) || (typeof duration === 'undefined' && animationOptions.duration !== 0))) { var animation = new Chart.Animation(); animation.numSteps = (duration || animationOptions.duration) / 16.66; // 60 fps @@ -411,15 +422,14 @@ module.exports = function(Chart) { // user events animation.onAnimationProgress = animationOptions.onProgress; - animation.onAnimationComplete = animationOptions.onComplete; + animation.onAnimationComplete = onComplete; Chart.animationService.addAnimation(me, animation, duration, lazy); } else { me.draw(); - if (animationOptions && animationOptions.onComplete && animationOptions.onComplete.call) { - animationOptions.onComplete.call(me); - } + onComplete(); } + return me; }, @@ -428,31 +438,45 @@ module.exports = function(Chart) { var easingDecimal = ease || 1; me.clear(); - Chart.plugins.notify(me, 'beforeDraw', [easingDecimal]); + plugins.notify(me, 'beforeDraw', [easingDecimal]); // Draw all the scales helpers.each(me.boxes, function(box) { box.draw(me.chartArea); }, me); + if (me.scale) { me.scale.draw(); } - Chart.plugins.notify(me, 'beforeDatasetsDraw', [easingDecimal]); + me.drawDatasets(easingDecimal); + + // Finally draw the tooltip + me.tooltip.transition(easingDecimal).draw(); + + plugins.notify(me, 'afterDraw', [easingDecimal]); + }, + + /** + * Draws all datasets unless a plugin returns `false` to the `beforeDatasetsDraw` + * hook, in which case, plugins will not be called on `afterDatasetsDraw`. + * @private + */ + drawDatasets: function(easingValue) { + var me = this; + + if (plugins.notify(me, 'beforeDatasetsDraw', [easingValue]) === false) { + return; + } // Draw each dataset via its respective controller (reversed to support proper line stacking) helpers.each(me.data.datasets, function(dataset, datasetIndex) { if (me.isDatasetVisible(datasetIndex)) { - me.getDatasetMeta(datasetIndex).controller.draw(ease); + me.getDatasetMeta(datasetIndex).controller.draw(easingValue); } }, me, true); - Chart.plugins.notify(me, 'afterDatasetsDraw', [easingDecimal]); - - // Finally draw the tooltip - me.tooltip.transition(easingDecimal).draw(); - - Chart.plugins.notify(me, 'afterDraw', [easingDecimal]); + plugins.notify(me, 'afterDatasetsDraw', [easingValue]); }, // Get the single element that was clicked on @@ -551,7 +575,7 @@ module.exports = function(Chart) { me.chart.ctx = null; } - Chart.plugins.notify(me, 'destroy'); + plugins.notify(me, 'destroy'); delete Chart.instances[me.id]; }, @@ -642,7 +666,7 @@ module.exports = function(Chart) { var changed = me.handleEvent(e); changed |= tooltip && tooltip.handleEvent(e); - changed |= Chart.plugins.notify(me, 'onEvent', [e]); + changed |= plugins.notify(me, 'onEvent', [e]); var bufferedRequest = me._bufferedRequest; if (bufferedRequest) { diff --git a/src/core/core.plugin.js b/src/core/core.plugin.js index 657c85a3545..f89b412d2e1 100644 --- a/src/core/core.plugin.js +++ b/src/core/core.plugin.js @@ -3,7 +3,6 @@ module.exports = function(Chart) { var helpers = Chart.helpers; - var noop = helpers.noop; Chart.defaults.global.plugins = {}; @@ -86,15 +85,15 @@ module.exports = function(Chart) { }, /** - * Calls enabled plugins for chart, on the specified extension and with the given args. + * 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 chart instance for which plugins should be called. - * @param {String} extension the name of the plugin method to call (e.g. 'beforeUpdate'). - * @param {Array} [args] extra arguments to apply to the extension call. + * @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, extension, args) { + notify: function(chart, hook, args) { var descriptors = this.descriptors(chart); var ilen = descriptors.length; var i, descriptor, plugin, params, method; @@ -102,7 +101,7 @@ module.exports = function(Chart) { for (i=0; i Date: Sun, 22 Jan 2017 21:01:46 +0100 Subject: [PATCH 58/61] Make `beforeDraw` cancellable and fix easing value --- src/core/core.controller.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/core/core.controller.js b/src/core/core.controller.js index e6c19659387..f67b29ff934 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -433,12 +433,18 @@ module.exports = function(Chart) { return me; }, - draw: function(ease) { + draw: function(easingValue) { var me = this; - var easingDecimal = ease || 1; + me.clear(); - plugins.notify(me, 'beforeDraw', [easingDecimal]); + if (easingValue === undefined || easingValue === null) { + easingValue = 1; + } + + if (plugins.notify(me, 'beforeDraw', [easingValue]) === false) { + return; + } // Draw all the scales helpers.each(me.boxes, function(box) { @@ -449,12 +455,12 @@ module.exports = function(Chart) { me.scale.draw(); } - me.drawDatasets(easingDecimal); + me.drawDatasets(easingValue); // Finally draw the tooltip - me.tooltip.transition(easingDecimal).draw(); + me.tooltip.transition(easingValue).draw(); - plugins.notify(me, 'afterDraw', [easingDecimal]); + plugins.notify(me, 'afterDraw', [easingValue]); }, /** From 7205ff5e2aa4515bae0c62bb9d8355745837270e Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sun, 22 Jan 2017 21:10:17 +0100 Subject: [PATCH 59/61] Replace `onEvent` by `before/afterEvent` --- docs/09-Advanced.md | 9 +++------ src/core/core.controller.js | 10 +++++++--- src/core/core.legend.js | 2 +- src/core/core.plugin.js | 16 ++++++++++++++++ test/platform.dom.tests.js | 2 +- 5 files changed, 28 insertions(+), 11 deletions(-) diff --git a/docs/09-Advanced.md b/docs/09-Advanced.md index d3d69d9afc9..05642fcab43 100644 --- a/docs/09-Advanced.md +++ b/docs/09-Advanced.md @@ -439,12 +439,9 @@ Plugins should implement the `IPlugin` interface: destroy: function(chartInstance) { } - /** - * Called when an event occurs on the chart - * @param e {Core.Event} the Chart.js wrapper around the native event. e.native is the original event - * @return {Boolean} true if the chart is changed and needs to re-render - */ - onEvent: function(chartInstance, e) {} + // Called when an event occurs on the chart + beforeEvent: function(chartInstance, event) {} + afterEvent: function(chartInstance, event) {} } ``` diff --git a/src/core/core.controller.js b/src/core/core.controller.js index f67b29ff934..b38df58b291 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -664,7 +664,10 @@ module.exports = function(Chart) { eventHandler: function(e) { var me = this; var tooltip = me.tooltip; - var hoverOptions = me.options.hover; + + if (plugins.notify(me, 'beforeEvent', [e]) === false) { + return; + } // Buffer any update calls so that renders do not occur me._bufferedRender = true; @@ -672,7 +675,8 @@ module.exports = function(Chart) { var changed = me.handleEvent(e); changed |= tooltip && tooltip.handleEvent(e); - changed |= plugins.notify(me, 'onEvent', [e]); + + plugins.notify(me, 'afterEvent', [e]); var bufferedRequest = me._bufferedRequest; if (bufferedRequest) { @@ -684,7 +688,7 @@ module.exports = function(Chart) { // We only need to render at this point. Updating will cause scales to be // recomputed generating flicker & using more memory than necessary. - me.render(hoverOptions.animationDuration, true); + me.render(me.options.hover.animationDuration, true); } me._bufferedRender = false; diff --git a/src/core/core.legend.js b/src/core/core.legend.js index b80bdbe25ad..4816b285f2e 100644 --- a/src/core/core.legend.js +++ b/src/core/core.legend.js @@ -526,7 +526,7 @@ module.exports = function(Chart) { delete chartInstance.legend; } }, - onEvent: function(chartInstance, e) { + afterEvent: function(chartInstance, e) { var legend = chartInstance.legend; if (legend) { legend.handleEvent(e); diff --git a/src/core/core.plugin.js b/src/core/core.plugin.js index f89b412d2e1..bda2ff45739 100644 --- a/src/core/core.plugin.js +++ b/src/core/core.plugin.js @@ -274,6 +274,22 @@ module.exports = function(Chart) { * @param {Number} 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. diff --git a/test/platform.dom.tests.js b/test/platform.dom.tests.js index 19c5bbe1aab..a022cc75d5a 100644 --- a/test/platform.dom.tests.js +++ b/test/platform.dom.tests.js @@ -320,7 +320,7 @@ describe('Platform.dom', function() { it('should notify plugins about events', function() { var notifiedEvent; var plugin = { - onEvent: function(chart, e) { + afterEvent: function(chart, e) { notifiedEvent = e; } }; From 9a3af51618b2627a328bdb66f0e3030522cd756e Mon Sep 17 00:00:00 2001 From: etimberg Date: Sat, 28 Jan 2017 11:28:36 -0500 Subject: [PATCH 60/61] bump version number --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 762f233d7c5..40eca264e17 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.4.0", + "version": "2.5.0", "license": "MIT", "main": "src/chart.js", "repository": { From 6ff34a5d4a29afea087f08568e01e2514e9d8b11 Mon Sep 17 00:00:00 2001 From: Matthisk Heimensen Date: Sat, 4 Feb 2017 00:17:33 +0100 Subject: [PATCH 61/61] Added Django-Jchart link to docs/notes.md (#3865) --- docs/10-Notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/10-Notes.md b/docs/10-Notes.md index e292a374046..451a8a8cc90 100644 --- a/docs/10-Notes.md +++ b/docs/10-Notes.md @@ -102,6 +102,7 @@ There are many extensions which are available for use with popular frameworks. S - react-chartjs-2 #### Django + - Django JChart - Django Chartjs #### Ruby on Rails