From 2bf2be7a9c58ee3461f74781bf76f707d64975de Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Wed, 29 Jun 2016 12:35:52 +0900 Subject: [PATCH 1/2] fix stacked scatter line chart --- src/controllers/controller.line.js | 14 +++--- test/controller.line.tests.js | 70 ++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 6 deletions(-) diff --git a/src/controllers/controller.line.js b/src/controllers/controller.line.js index 36b4d8f54..f3d7328d4 100644 --- a/src/controllers/controller.line.js +++ b/src/controllers/controller.line.js @@ -221,18 +221,20 @@ module.exports = function(Chart) { ds = chart.data.datasets[i]; dsMeta = chart.getDatasetMeta(i); if (dsMeta.type === 'line' && chart.isDatasetVisible(i)) { - if (ds.data[index] < 0) { - sumNeg += ds.data[index] || 0; + var stackedRightValue = yScale.getRightValue(ds.data[index]); + if (stackedRightValue < 0) { + sumNeg += stackedRightValue || 0; } else { - sumPos += ds.data[index] || 0; + sumPos += stackedRightValue || 0; } } } - if (value < 0) { - return yScale.getPixelForValue(sumNeg + value); + var rightValue = yScale.getRightValue(value); + if (rightValue < 0) { + return yScale.getPixelForValue(sumNeg + rightValue); } else { - return yScale.getPixelForValue(sumPos + value); + return yScale.getPixelForValue(sumPos + rightValue); } } diff --git a/test/controller.line.tests.js b/test/controller.line.tests.js index 782cf8f43..78aecf2d2 100644 --- a/test/controller.line.tests.js +++ b/test/controller.line.tests.js @@ -278,6 +278,76 @@ describe('Line controller tests', function() { }); + it('should update elements when the y scale is stacked and datasets is scatter data', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [{ + x: 0, + y: 10 + }, { + x: 1, + y: -10 + }, { + x: 2, + y: 10 + }, { + x: 3, + y: -10 + }], + label: 'dataset1' + }, { + data: [{ + x: 0, + y: 10 + }, { + x: 1, + y: 15 + }, { + x: 2, + y: 0 + }, { + x: 3, + y: -4 + }], + label: 'dataset2' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + scales: { + yAxes: [{ + stacked: true + }] + } + } + }); + + var meta0 = chart.getDatasetMeta(0); + + [ { x: 38, y: 161 }, + { x: 189, y: 419 }, + { x: 341, y: 161 }, + { x: 492, y: 419 } + ].forEach(function(values, i) { + expect(meta0.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta0.data[i]._model.y).toBeCloseToPixel(values.y); + }); + + var meta1 = chart.getDatasetMeta(1); + + [ { x: 38, y: 32 }, + { x: 189, y: 97 }, + { x: 341, y: 161 }, + { x: 492, y: 471 } + ].forEach(function(values, i) { + expect(meta1.data[i]._model.x).toBeCloseToPixel(values.x); + expect(meta1.data[i]._model.y).toBeCloseToPixel(values.y); + }); + + }); + it('should find the correct scale zero when the data is all positive', function() { var chart = window.acquireChart({ type: 'line', From 0dccc85e3a0f6a9e2fa2cb6e4c39533dcbc653d5 Mon Sep 17 00:00:00 2001 From: Shayne Linhart Date: Tue, 5 Jul 2016 20:08:29 -0600 Subject: [PATCH 2/2] Added usePointStyle option to label boxes - Closes #2252 - Allows label boxes to match the shape(pointStyle) of the corresponding data. * Removed unused varaible from legend's draw() --- docs/01-Chart-Configuration.md | 4 ++ src/chart.js | 1 + src/core/core.canvasHelpers.js | 104 +++++++++++++++++++++++++++++++++ src/core/core.legend.js | 52 +++++++++++++---- src/elements/element.point.js | 94 +---------------------------- test/core.legend.tests.js | 4 ++ test/element.point.tests.js | 3 + 7 files changed, 157 insertions(+), 105 deletions(-) create mode 100644 src/core/core.canvasHelpers.js diff --git a/docs/01-Chart-Configuration.md b/docs/01-Chart-Configuration.md index 75950eada..9c9f3659d 100644 --- a/docs/01-Chart-Configuration.md +++ b/docs/01-Chart-Configuration.md @@ -132,6 +132,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. +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 @@ -165,6 +166,9 @@ Items passed to the legend `onClick` function are the ones returned from `labels // Stroke style of the legend box strokeStyle: Color + + // Point style of the legend box (only used if usePointStyle is true) + pointStyle: String } ``` diff --git a/src/chart.js b/src/chart.js index 75bd47538..a12890aa0 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('./core/core.canvasHelpers')(Chart); require('./core/core.element')(Chart); require('./core/core.animation')(Chart); require('./core/core.controller')(Chart); diff --git a/src/core/core.canvasHelpers.js b/src/core/core.canvasHelpers.js new file mode 100644 index 000000000..f8c35c96e --- /dev/null +++ b/src/core/core.canvasHelpers.js @@ -0,0 +1,104 @@ +"use strict"; + +module.exports = function(Chart) { + // Global Chart canvas helpers object for drawing items to canvas + var helpers = Chart.canvasHelpers = {}; + + helpers.drawPoint = function(ctx, pointStyle, radius, x, y) { + var type, edgeLength, xOffset, yOffset, height, size; + + if (typeof pointStyle === 'object') { + type = pointStyle.toString(); + if (type === '[object HTMLImageElement]' || type === '[object HTMLCanvasElement]') { + ctx.drawImage(pointStyle, x - pointStyle.width / 2, y - pointStyle.height / 2); + return; + } + } + + if (isNaN(radius) || radius <= 0) { + return; + } + + switch (pointStyle) { + // Default includes circle + default: + ctx.beginPath(); + ctx.arc(x, y, radius, 0, Math.PI * 2); + ctx.closePath(); + ctx.fill(); + break; + case 'triangle': + ctx.beginPath(); + edgeLength = 3 * radius / Math.sqrt(3); + height = edgeLength * Math.sqrt(3) / 2; + ctx.moveTo(x - edgeLength / 2, y + height / 3); + ctx.lineTo(x + edgeLength / 2, y + height / 3); + ctx.lineTo(x, y - 2 * height / 3); + ctx.closePath(); + ctx.fill(); + break; + case 'rect': + size = 1 / Math.SQRT2 * radius; + ctx.beginPath(); + ctx.fillRect(x - size, y - size, 2 * size, 2 * size); + ctx.strokeRect(x - size, y - size, 2 * size, 2 * size); + break; + case 'rectRot': + size = 1 / Math.SQRT2 * radius; + ctx.beginPath(); + ctx.moveTo(x - size, y); + ctx.lineTo(x, y + size); + ctx.lineTo(x + size, y); + ctx.lineTo(x, y - size); + ctx.closePath(); + ctx.fill(); + break; + case 'cross': + ctx.beginPath(); + ctx.moveTo(x, y + radius); + ctx.lineTo(x, y - radius); + ctx.moveTo(x - radius, y); + ctx.lineTo(x + radius, y); + ctx.closePath(); + break; + case 'crossRot': + ctx.beginPath(); + xOffset = Math.cos(Math.PI / 4) * radius; + yOffset = Math.sin(Math.PI / 4) * radius; + ctx.moveTo(x - xOffset, y - yOffset); + ctx.lineTo(x + xOffset, y + yOffset); + ctx.moveTo(x - xOffset, y + yOffset); + ctx.lineTo(x + xOffset, y - yOffset); + ctx.closePath(); + break; + case 'star': + ctx.beginPath(); + ctx.moveTo(x, y + radius); + ctx.lineTo(x, y - radius); + ctx.moveTo(x - radius, y); + ctx.lineTo(x + radius, y); + xOffset = Math.cos(Math.PI / 4) * radius; + yOffset = Math.sin(Math.PI / 4) * radius; + ctx.moveTo(x - xOffset, y - yOffset); + ctx.lineTo(x + xOffset, y + yOffset); + ctx.moveTo(x - xOffset, y + yOffset); + ctx.lineTo(x + xOffset, y - yOffset); + ctx.closePath(); + break; + case 'line': + ctx.beginPath(); + ctx.moveTo(x - radius, y); + ctx.lineTo(x + radius, y); + ctx.closePath(); + break; + case 'dash': + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.lineTo(x + radius, y); + ctx.closePath(); + break; + } + + ctx.stroke(); + }; +}; \ No newline at end of file diff --git a/src/core/core.legend.js b/src/core/core.legend.js index 4747bbad5..a13151bd3 100644 --- a/src/core/core.legend.js +++ b/src/core/core.legend.js @@ -52,6 +52,7 @@ module.exports = function(Chart) { lineJoin: dataset.borderJoinStyle, lineWidth: dataset.borderWidth, strokeStyle: dataset.borderColor, + pointStyle: dataset.pointStyle, // Below is extra data used for toggling the datasets datasetIndex: i @@ -201,7 +202,11 @@ module.exports = function(Chart) { ctx.textBaseline = 'top'; helpers.each(me.legendItems, function(legendItem, i) { - var width = labelOpts.boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; + var boxWidth = labelOpts.usePointStyle ? + fontSize * Math.sqrt(2) : + labelOpts.boxWidth; + + var width = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; if (lineWidths[lineWidths.length - 1] + width + labelOpts.padding >= me.width) { totalHeight += fontSize + (labelOpts.padding); lineWidths[lineWidths.length] = me.left; @@ -229,7 +234,11 @@ module.exports = function(Chart) { var itemHeight = fontSize + vPadding; helpers.each(me.legendItems, function(legendItem, i) { - var itemWidth = labelOpts.boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; + // If usePointStyle is set, multiple boxWidth by 2 since it represents + // the radius and not truly the width + var boxWidth = labelOpts.usePointStyle ? 2 * labelOpts.boxWidth : labelOpts.boxWidth; + + var itemWidth = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; // If too tall, go to new column if (currentColHeight + itemHeight > minSize.height) { @@ -274,10 +283,10 @@ module.exports = function(Chart) { var me = this; var opts = me.options; var labelOpts = opts.labels; - var globalDefault = Chart.defaults.global; - var lineDefault = globalDefault.elements.line; - var legendWidth = me.width; - var lineWidths = me.lineWidths; + var globalDefault = Chart.defaults.global, + lineDefault = globalDefault.elements.line, + legendWidth = me.width, + lineWidths = me.lineWidths; if (opts.display) { var ctx = me.ctx, @@ -302,6 +311,10 @@ module.exports = function(Chart) { // current position var drawLegendBox = function(x, y, legendItem) { + if (isNaN(boxWidth) || boxWidth <= 0) { + return; + } + // Set the ctx for the box ctx.save(); @@ -317,9 +330,22 @@ module.exports = function(Chart) { ctx.setLineDash(itemOrDefault(legendItem.lineDash, lineDefault.borderDash)); } - // Draw the box - ctx.strokeRect(x, y, boxWidth, fontSize); - ctx.fillRect(x, y, boxWidth, fontSize); + if (opts.labels && opts.labels.usePointStyle) { + // Recalulate x and y for drawPoint() because its expecting + // x and y to be center of figure (instead of top left) + var radius = fontSize * Math.SQRT2 / 2; + var offSet = radius / Math.SQRT2; + var centerX = x + offSet; + var centerY = y + offSet; + + // Draw pointStyle as legend symbol + Chart.canvasHelpers.drawPoint(ctx, legendItem.pointStyle, radius, centerX, centerY); + } + else { + // Draw box as legend symbol + ctx.strokeRect(x, y, boxWidth, fontSize); + ctx.fillRect(x, y, boxWidth, fontSize); + } ctx.restore(); }; @@ -347,7 +373,7 @@ module.exports = function(Chart) { } else { cursor = { x: me.left + labelOpts.padding, - y: me.top, + y: me.top + labelOpts.padding, line: 0 }; } @@ -355,13 +381,15 @@ module.exports = function(Chart) { var itemHeight = fontSize + labelOpts.padding; helpers.each(me.legendItems, function(legendItem, i) { var textWidth = ctx.measureText(legendItem.text).width, - width = boxWidth + (fontSize / 2) + textWidth, + width = labelOpts.usePointStyle ? + fontSize + (fontSize / 2) + textWidth : + boxWidth + (fontSize / 2) + textWidth, x = cursor.x, y = cursor.y; if (isHorizontal) { if (x + width >= legendWidth) { - y = cursor.y += fontSize + (labelOpts.padding); + y = cursor.y += itemHeight; cursor.line++; x = cursor.x = me.left + ((legendWidth - lineWidths[cursor.line]) / 2); } diff --git a/src/elements/element.point.js b/src/elements/element.point.js index 3290041b2..4131f8ced 100644 --- a/src/elements/element.point.js +++ b/src/elements/element.point.js @@ -42,108 +42,16 @@ module.exports = function(Chart) { var radius = vm.radius; var x = vm.x; var y = vm.y; - var type, edgeLength, xOffset, yOffset, height, size; if (vm.skip) { return; } - if (typeof pointStyle === 'object') { - type = pointStyle.toString(); - if (type === '[object HTMLImageElement]' || type === '[object HTMLCanvasElement]') { - ctx.drawImage(pointStyle, x - pointStyle.width / 2, y - pointStyle.height / 2); - return; - } - } - - if (isNaN(radius) || radius <= 0) { - return; - } - ctx.strokeStyle = vm.borderColor || defaultColor; ctx.lineWidth = helpers.getValueOrDefault(vm.borderWidth, globalOpts.elements.point.borderWidth); ctx.fillStyle = vm.backgroundColor || defaultColor; - switch (pointStyle) { - // Default includes circle - default: - ctx.beginPath(); - ctx.arc(x, y, radius, 0, Math.PI * 2); - ctx.closePath(); - ctx.fill(); - break; - case 'triangle': - ctx.beginPath(); - edgeLength = 3 * radius / Math.sqrt(3); - height = edgeLength * Math.sqrt(3) / 2; - ctx.moveTo(x - edgeLength / 2, y + height / 3); - ctx.lineTo(x + edgeLength / 2, y + height / 3); - ctx.lineTo(x, y - 2 * height / 3); - ctx.closePath(); - ctx.fill(); - break; - case 'rect': - size = 1 / Math.SQRT2 * radius; - ctx.fillRect(x - size, y - size, 2 * size, 2 * size); - ctx.strokeRect(x - size, y - size, 2 * size, 2 * size); - break; - case 'rectRot': - size = 1 / Math.SQRT2 * radius; - ctx.beginPath(); - ctx.moveTo(x - size, y); - ctx.lineTo(x, y + size); - ctx.lineTo(x + size, y); - ctx.lineTo(x, y - size); - ctx.closePath(); - ctx.fill(); - break; - case 'cross': - ctx.beginPath(); - ctx.moveTo(x, y + radius); - ctx.lineTo(x, y - radius); - ctx.moveTo(x - radius, y); - ctx.lineTo(x + radius, y); - ctx.closePath(); - break; - case 'crossRot': - ctx.beginPath(); - xOffset = Math.cos(Math.PI / 4) * radius; - yOffset = Math.sin(Math.PI / 4) * radius; - ctx.moveTo(x - xOffset, y - yOffset); - ctx.lineTo(x + xOffset, y + yOffset); - ctx.moveTo(x - xOffset, y + yOffset); - ctx.lineTo(x + xOffset, y - yOffset); - ctx.closePath(); - break; - case 'star': - ctx.beginPath(); - ctx.moveTo(x, y + radius); - ctx.lineTo(x, y - radius); - ctx.moveTo(x - radius, y); - ctx.lineTo(x + radius, y); - xOffset = Math.cos(Math.PI / 4) * radius; - yOffset = Math.sin(Math.PI / 4) * radius; - ctx.moveTo(x - xOffset, y - yOffset); - ctx.lineTo(x + xOffset, y + yOffset); - ctx.moveTo(x - xOffset, y + yOffset); - ctx.lineTo(x + xOffset, y - yOffset); - ctx.closePath(); - break; - case 'line': - ctx.beginPath(); - ctx.moveTo(x - radius, y); - ctx.lineTo(x + radius, y); - ctx.closePath(); - break; - case 'dash': - ctx.beginPath(); - ctx.moveTo(x, y); - ctx.lineTo(x + radius, y); - ctx.closePath(); - break; - } - - ctx.stroke(); + Chart.canvasHelpers.drawPoint(ctx, pointStyle, radius, x, y); } }); }; diff --git a/test/core.legend.tests.js b/test/core.legend.tests.js index fa6e8e76f..2db970f3f 100644 --- a/test/core.legend.tests.js +++ b/test/core.legend.tests.js @@ -52,6 +52,7 @@ describe('Legend block tests', function() { label: 'dataset3', borderWidth: 10, borderColor: 'green', + pointStyle: 'crossRot', data: [] }], labels: [] @@ -68,6 +69,7 @@ describe('Legend block tests', function() { lineJoin: undefined, lineWidth: undefined, strokeStyle: undefined, + pointStyle: undefined, datasetIndex: 0 }, { text: 'dataset2', @@ -79,6 +81,7 @@ describe('Legend block tests', function() { lineJoin: 'miter', lineWidth: undefined, strokeStyle: undefined, + pointStyle: undefined, datasetIndex: 1 }, { text: 'dataset3', @@ -90,6 +93,7 @@ describe('Legend block tests', function() { lineJoin: undefined, lineWidth: 10, strokeStyle: 'green', + pointStyle: 'crossRot', datasetIndex: 2 }]); }); diff --git a/test/element.point.tests.js b/test/element.point.tests.js index 674682660..c257f7375 100644 --- a/test/element.point.tests.js +++ b/test/element.point.tests.js @@ -164,6 +164,9 @@ describe('Point element tests', function() { }, { name: 'setFillStyle', args: ['rgba(0, 255, 0)'] + }, { + name: 'beginPath', + args: [] }, { name: 'fillRect', args: [10 - 1 / Math.SQRT2 * 2, 15 - 1 / Math.SQRT2 * 2, 2 / Math.SQRT2 * 2, 2 / Math.SQRT2 * 2]