Skip to content

Commit

Permalink
Enable arbitrary rotation of datapoints (#5319)
Browse files Browse the repository at this point in the history
  • Loading branch information
joel-hamilton authored and simonbrunel committed Jul 7, 2018
1 parent 387a23d commit 0ddd0ee
Show file tree
Hide file tree
Showing 10 changed files with 218 additions and 69 deletions.
2 changes: 2 additions & 0 deletions docs/charts/bubble.md
Expand Up @@ -51,6 +51,7 @@ The bubble chart allows a number of properties to be specified for each dataset.
| [`hitRadius`](#interactions) | `Number` | Yes | Yes | `1`
| [`label`](#labeling) | `String` | - | - | `undefined`
| [`pointStyle`](#styling) | `String` | Yes | Yes | `circle`
| [`rotation`](#styling) | `Number` | Yes | Yes | `0`
| [`radius`](#styling) | `Number` | Yes | Yes | `3`

### Labeling
Expand All @@ -67,6 +68,7 @@ The style of each bubble can be controlled with the following properties:
| `borderColor` | bubble border color
| `borderWidth` | bubble border width (in pixels)
| `pointStyle` | bubble [shape style](../configuration/elements#point-styles)
| `rotation` | bubble rotation (in degrees)
| `radius` | bubble radius (in pixels)

All these values, if `undefined`, fallback to the associated [`elements.point.*`](../configuration/elements.md#point-configuration) options.
Expand Down
1 change: 1 addition & 0 deletions docs/charts/line.md
Expand Up @@ -63,6 +63,7 @@ All point* properties can be specified as an array. If these are set to an array
| `pointBorderWidth` | `Number/Number[]` | The width of the point border in pixels.
| `pointRadius` | `Number/Number[]` | The radius of the point shape. If set to 0, the point is not rendered.
| `pointStyle` | `String/String[]/Image/Image[]` | Style of the point. [more...](../configuration/elements#point-styles)
| `pointRotation` | `Number/Number[]` | The rotation of the point in degrees.
| `pointHitRadius` | `Number/Number[]` | The pixel size of the non-displayed point that reacts to mouse events.
| `pointHoverBackgroundColor` | `Color/Color[]` | Point background color when hovered.
| `pointHoverBorderColor` | `Color/Color[]` | Point border color when hovered.
Expand Down
1 change: 1 addition & 0 deletions docs/charts/radar.md
Expand Up @@ -82,6 +82,7 @@ All point* properties can be specified as an array. If these are set to an array
| `pointBorderColor` | `Color/Color[]` | The border color for points.
| `pointBorderWidth` | `Number/Number[]` | The width of the point border in pixels.
| `pointRadius` | `Number/Number[]` | The radius of the point shape. If set to 0, the point is not rendered.
| `pointRotation` | `Number/Number[]` | The rotation of the point in degrees.
| `pointStyle` | `String/String[]/Image/Image[]` | Style of the point. [more...](#pointstyle)
| `pointHitRadius` | `Number/Number[]` | The pixel size of the non-displayed point that reacts to mouse events.
| `pointHoverBackgroundColor` | `Color/Color[]` | Point background color when hovered.
Expand Down
1 change: 1 addition & 0 deletions docs/configuration/elements.md
Expand Up @@ -19,6 +19,7 @@ Global point options: `Chart.defaults.global.elements.point`
| -----| ---- | --------| -----------
| `radius` | `Number` | `3` | Point radius.
| [`pointStyle`](#point-styles) | `String` | `circle` | Point style.
| `rotation` | `Number` | `0` | Point rotation (in degrees).
| `backgroundColor` | `Color` | `'rgba(0,0,0,0.1)'` | Point fill color.
| `borderWidth` | `Number` | `1` | Point stroke width.
| `borderColor` | `Color` | `'rgba(0,0,0,0.1)'` | Point stroke color.
Expand Down
5 changes: 3 additions & 2 deletions src/controllers/controller.bubble.js
Expand Up @@ -87,6 +87,7 @@ module.exports = function(Chart) {
borderWidth: options.borderWidth,
hitRadius: options.hitRadius,
pointStyle: options.pointStyle,
rotation: options.rotation,
radius: reset ? 0 : options.radius,
skip: custom.skip || isNaN(x) || isNaN(y),
x: x,
Expand Down Expand Up @@ -146,7 +147,8 @@ module.exports = function(Chart) {
'hoverBorderWidth',
'hoverRadius',
'hitRadius',
'pointStyle'
'pointStyle',
'rotation'
];

for (i = 0, ilen = keys.length; i < ilen; ++i) {
Expand All @@ -165,7 +167,6 @@ module.exports = function(Chart) {
dataset.radius,
options.radius
], context, index);

return values;
}
});
Expand Down
14 changes: 14 additions & 0 deletions src/controllers/controller.line.js
Expand Up @@ -148,6 +148,19 @@ module.exports = function(Chart) {
return borderWidth;
},

getPointRotation: function(point, index) {
var pointRotation = this.chart.options.elements.point.rotation;
var dataset = this.getDataset();
var custom = point.custom || {};

if (!isNaN(custom.rotation)) {
pointRotation = custom.rotation;
} else if (!isNaN(dataset.pointRotation) || helpers.isArray(dataset.pointRotation)) {
pointRotation = helpers.valueAtIndexOrDefault(dataset.pointRotation, index, pointRotation);
}
return pointRotation;
},

updateElement: function(point, index, reset) {
var me = this;
var meta = me.getMeta();
Expand Down Expand Up @@ -185,6 +198,7 @@ module.exports = function(Chart) {
// Appearance
radius: custom.radius || helpers.valueAtIndexOrDefault(dataset.pointRadius, index, pointOptions.radius),
pointStyle: custom.pointStyle || helpers.valueAtIndexOrDefault(dataset.pointStyle, index, pointOptions.pointStyle),
rotation: me.getPointRotation(point, index),
backgroundColor: me.getPointBackgroundColor(point, index),
borderColor: me.getPointBorderColor(point, index),
borderWidth: me.getPointBorderWidth(point, index),
Expand Down
1 change: 1 addition & 0 deletions src/controllers/controller.radar.js
Expand Up @@ -106,6 +106,7 @@ module.exports = function(Chart) {
borderColor: custom.borderColor ? custom.borderColor : helpers.valueAtIndexOrDefault(dataset.pointBorderColor, index, pointElementOptions.borderColor),
borderWidth: custom.borderWidth ? custom.borderWidth : helpers.valueAtIndexOrDefault(dataset.pointBorderWidth, index, pointElementOptions.borderWidth),
pointStyle: custom.pointStyle ? custom.pointStyle : helpers.valueAtIndexOrDefault(dataset.pointStyle, index, pointElementOptions.pointStyle),
rotation: custom.rotation ? custom.rotation : helpers.valueAtIndexOrDefault(dataset.pointRotation, index, pointElementOptions.rotation),

// Tooltip
hitRadius: custom.hitRadius ? custom.hitRadius : helpers.valueAtIndexOrDefault(dataset.pointHitRadius, index, pointElementOptions.hitRadius)
Expand Down
3 changes: 2 additions & 1 deletion src/elements/element.point.js
Expand Up @@ -68,6 +68,7 @@ module.exports = Element.extend({
var model = this._model;
var ctx = this._chart.ctx;
var pointStyle = vm.pointStyle;
var rotation = vm.rotation;
var radius = vm.radius;
var x = vm.x;
var y = vm.y;
Expand All @@ -82,7 +83,7 @@ module.exports = Element.extend({
ctx.strokeStyle = vm.borderColor || defaultColor;
ctx.lineWidth = helpers.valueOrDefault(vm.borderWidth, defaults.global.elements.point.borderWidth);
ctx.fillStyle = vm.backgroundColor || defaultColor;
helpers.canvas.drawPoint(ctx, pointStyle, radius, x, y);
helpers.canvas.drawPoint(ctx, pointStyle, radius, x, y, rotation);
}
}
});
72 changes: 39 additions & 33 deletions src/helpers/helpers.canvas.js
Expand Up @@ -46,8 +46,9 @@ var exports = module.exports = {
}
},

drawPoint: function(ctx, style, radius, x, y) {
drawPoint: function(ctx, style, radius, x, y, rotation) {
var type, edgeLength, xOffset, yOffset, height, size;
rotation = rotation || 0;

if (style && typeof style === 'object') {
type = style.toString();
Expand All @@ -61,34 +62,38 @@ var exports = module.exports = {
return;
}

ctx.save();
ctx.translate(x, y);
ctx.rotate(rotation * Math.PI / 180);

switch (style) {
// Default includes circle
default:
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.arc(0, 0, 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.moveTo(-edgeLength / 2, height / 3);
ctx.lineTo(edgeLength / 2, height / 3);
ctx.lineTo(0, -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);
ctx.fillRect(-size, -size, 2 * size, 2 * size);
ctx.strokeRect(-size, -size, 2 * size, 2 * size);
break;
case 'rectRounded':
var offset = radius / Math.SQRT2;
var leftX = x - offset;
var topY = y - offset;
var leftX = -offset;
var topY = -offset;
var sideSize = Math.SQRT2 * radius;
ctx.beginPath();

Expand All @@ -104,60 +109,61 @@ var exports = module.exports = {
case 'rectRot':
size = 1 / Math.SQRT2 * radius;
ctx.beginPath();
ctx.moveTo(x - size, y);
ctx.lineTo(x, y + size);
ctx.lineTo(x + size, y);
ctx.lineTo(x, y - size);
ctx.moveTo(-size, 0);
ctx.lineTo(0, size);
ctx.lineTo(size, 0);
ctx.lineTo(0, -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.moveTo(0, radius);
ctx.lineTo(0, -radius);
ctx.moveTo(-radius, 0);
ctx.lineTo(radius, 0);
ctx.closePath();
break;
case 'crossRot':
ctx.beginPath();
xOffset = Math.cos(Math.PI / 4) * radius;
yOffset = Math.sin(Math.PI / 4) * radius;
ctx.moveTo(x - xOffset, y - yOffset);
ctx.lineTo(x + xOffset, y + yOffset);
ctx.moveTo(x - xOffset, y + yOffset);
ctx.lineTo(x + xOffset, y - yOffset);
ctx.moveTo(-xOffset, -yOffset);
ctx.lineTo(xOffset, yOffset);
ctx.moveTo(-xOffset, yOffset);
ctx.lineTo(xOffset, -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);
ctx.moveTo(0, radius);
ctx.lineTo(0, -radius);
ctx.moveTo(-radius, 0);
ctx.lineTo(radius, 0);
xOffset = Math.cos(Math.PI / 4) * radius;
yOffset = Math.sin(Math.PI / 4) * radius;
ctx.moveTo(x - xOffset, y - yOffset);
ctx.lineTo(x + xOffset, y + yOffset);
ctx.moveTo(x - xOffset, y + yOffset);
ctx.lineTo(x + xOffset, y - yOffset);
ctx.moveTo(-xOffset, -yOffset);
ctx.lineTo(xOffset, yOffset);
ctx.moveTo(-xOffset, yOffset);
ctx.lineTo(xOffset, -yOffset);
ctx.closePath();
break;
case 'line':
ctx.beginPath();
ctx.moveTo(x - radius, y);
ctx.lineTo(x + radius, y);
ctx.moveTo(-radius, 0);
ctx.lineTo(radius, 0);
ctx.closePath();
break;
case 'dash':
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + radius, y);
ctx.moveTo(0, 0);
ctx.lineTo(radius, 0);
ctx.closePath();
break;
}

ctx.stroke();
ctx.restore();
},

clipArea: function(ctx, area) {
Expand Down

2 comments on commit 0ddd0ee

@sfrauenfelder
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, I'm just new to Github, so I hope I am doing this the right way.
I appreciate the rotated point very much (I had hacked into my copy of Chartjs myself the last couple of days :-)).
I would like to suggest to add the possibility of specifying the point-for-point rotation within the data series as a third value, just like the radius in the bubble chart. So, for each data point, we would specify
{x: 10, y: 14, a: 315} (a for angle).
This is nice for displaying wind information: we can combine wind speed and direction in one chart!

I hope this serves a purpose.

@simonbrunel
Copy link
Member

@simonbrunel simonbrunel commented on 0ddd0ee Aug 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sfrauenfelder Comments in code are not ideal to keep track of feature requests, it would be better to create an issue with a good description of your use case, then we can discuss about the implementation.

Though, I'm not sure it should be part of the dataset data but instead should be a scriptable option (as it's already the case for the bubble chart). Then we can handle more use cases and data structures:

pointRotation: function(ctx) {
    // compute angle based on whatever is your use case:
    // return ctx.dataset.data[ctx.dataIndex].a; // or .angle, or .rotation, or ...
    // return ctx.dataset.data[ctx.dataIndex].x > 42 ? 90 : -90;
    // return ctx.dataIndex % 4 ? 45 : 0;
}

Scriptable options are only available for bubble charts but should be implemented for all charts.

Please sign in to comment.