Add offset option for arc (#7691)

* Add offset option for arc
* Finishing touches
This commit is contained in:
Jukka Kurkela
2020-08-05 14:35:28 +03:00
committed by Evert Timberg
parent 8fadae6c91
commit 483d06eebd
5 changed files with 138 additions and 88 deletions

View File

@@ -10,9 +10,22 @@ Pie and doughnut charts are effectively the same class in Chart.js, but have one
They are also registered under two aliases in the `Chart` core. Other than their different default value, and different alias, they are exactly the same.
import { useEffect } from 'react';
import { useEffect, useRef } from 'react';
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
export const ExampleChart = () => {
<Tabs
defaultValue='doughnut'
values={[
{label: 'Doughnut', value: 'doughnut' },
{label: 'Pie', value: 'pie' },
]}
>
<TabItem value="doughnut">
```jsx live
function example() {
const canvas = useRef(null);
useEffect(() => {
const cfg = {
type: 'doughnut',
@@ -29,37 +42,54 @@ export const ExampleChart = () => {
'rgb(255, 99, 132)',
'rgb(54, 162, 235)',
'rgb(255, 205, 86)'
]
],
hoverOffset: 4
}]
}
};
new Chart(document.getElementById('chartjs-0').getContext('2d'), cfg);
new Chart(canvas.current.getContext('2d'), cfg);
});
return <div className="chartjs-wrapper"><canvas id="chartjs-0" className="chartjs"></canvas></div>;
return <div className="chartjs-wrapper"><canvas ref={canvas} className="chartjs"></canvas></div>;
}
<ExampleChart/>
## Example Usage
```javascript
// For a pie chart
var myPieChart = new Chart(ctx, {
type: 'pie',
data: data,
options: options
});
```
```javascript
// And for a doughnut chart
var myDoughnutChart = new Chart(ctx, {
type: 'doughnut',
data: data,
options: options
});
</TabItem>
<TabItem value="pie">
```jsx live
function example() {
const canvas = useRef(null);
useEffect(() => {
const cfg = {
type: 'pie',
data: {
labels: [
'Red',
'Blue',
'Yellow'
],
datasets: [{
label: 'My First Dataset',
data: [300, 50, 100],
backgroundColor: [
'rgb(255, 99, 132)',
'rgb(54, 162, 235)',
'rgb(255, 205, 86)'
],
hoverOffset: 4
}]
}
};
new Chart(canvas.current.getContext('2d'), cfg);
});
return <div className="chartjs-wrapper"><canvas ref={canvas} className="chartjs"></canvas></div>;
}
```
</TabItem>
</Tabs>
## Dataset Properties
The doughnut/pie chart allows a number of properties to be specified for each dataset. These are used to set display properties for a specific dataset. For example, the colours of the dataset's arcs are generally set this way.
@@ -75,6 +105,8 @@ The doughnut/pie chart allows a number of properties to be specified for each da
| [`hoverBackgroundColor`](#interations) | [`Color`](../general/colors.md) | Yes | Yes | `undefined`
| [`hoverBorderColor`](#interactions) | [`Color`](../general/colors.md) | Yes | Yes | `undefined`
| [`hoverBorderWidth`](#interactions) | `number` | Yes | Yes | `undefined`
| [`hoverOffset`](#interactions) | `number` | Yes | Yes | `0`
| [`offset`](#styling) | `number` | Yes | Yes | `0`
| [`weight`](#styling) | `number` | - | - | `1`
### General
@@ -93,6 +125,7 @@ The style of each arc can be controlled with the following properties:
| `backgroundColor` | arc background color.
| `borderColor` | arc border color.
| `borderWidth` | arc border width (in pixels).
| `offset` | arc offset (in pixels).
| `weight` | The relative thickness of the dataset. Providing a value for weight will cause the pie or doughnut dataset to be drawn with a thickness relative to the sum of all the dataset weight values.
All these values, if `undefined`, fallback to the associated [`elements.arc.*`](../configuration/elements.md#arc-configuration) options.
@@ -114,6 +147,7 @@ The interaction with each arc can be controlled with the following properties:
| `hoverBackgroundColor` | arc background color when hovered.
| `hoverBorderColor` | arc border color when hovered.
| `hoverBorderWidth` | arc border width when hovered (in pixels).
| `hoverOffset` | arc offset when hovered (in pixels).
All these values, if `undefined`, fallback to the associated [`elements.arc.*`](../configuration/elements.md#arc-configuration) options.

View File

@@ -89,9 +89,9 @@ export default class DoughnutController extends DatasetController {
const cutout = options.cutoutPercentage / 100 || 0;
const chartWeight = me._getRingWeight(me.index);
const {ratioX, ratioY, offsetX, offsetY} = getRatioAndOffset(options.rotation, options.circumference, cutout);
const borderWidth = me.getMaxBorderWidth();
const maxWidth = (chartArea.right - chartArea.left - borderWidth) / ratioX;
const maxHeight = (chartArea.bottom - chartArea.top - borderWidth) / ratioY;
const spacing = me.getMaxBorderWidth() + me.getMaxOffset(arcs);
const maxWidth = (chartArea.right - chartArea.left - spacing) / ratioX;
const maxHeight = (chartArea.bottom - chartArea.top - spacing) / ratioY;
const outerRadius = Math.max(Math.min(maxWidth, maxHeight) / 2, 0);
const innerRadius = Math.max(outerRadius * cutout, 0);
const radiusLength = (outerRadius - innerRadius) / me._getVisibleDatasetWeightTotal();
@@ -219,6 +219,16 @@ export default class DoughnutController extends DatasetController {
return max;
}
getMaxOffset(arcs) {
let max = 0;
for (let i = 0, ilen = arcs.length; i < ilen; ++i) {
const options = this.resolveDataElementOptions(i);
max = Math.max(max, options.offset || 0, options.hoverOffset || 0);
}
return max;
}
/**
* Get radius length offset of the dataset in relation to the visible datasets weights. This allows determining the inner and outer radius correctly
* @private
@@ -264,14 +274,12 @@ DoughnutController.defaults = {
'borderColor',
'borderWidth',
'borderAlign',
'hoverBackgroundColor',
'hoverBorderColor',
'hoverBorderWidth',
'offset'
],
animation: {
numbers: {
type: 'number',
properties: ['circumference', 'endAngle', 'innerRadius', 'outerRadius', 'startAngle', 'x', 'y']
properties: ['circumference', 'endAngle', 'innerRadius', 'outerRadius', 'startAngle', 'x', 'y', 'offset', 'borderWidth']
},
// Boolean - Whether we animate the rotation of the Doughnut
animateRotate: true,

View File

@@ -148,9 +148,7 @@ PolarAreaController.defaults = {
'borderColor',
'borderWidth',
'borderAlign',
'hoverBackgroundColor',
'hoverBorderColor',
'hoverBorderWidth'
'offset'
],
animation: {

View File

@@ -714,7 +714,7 @@ export default class DatasetController {
/**
* @param {number} index
* @param {string} mode
* @param {string} [mode]
* @protected
*/
resolveDataElementOptions(index, mode) {

View File

@@ -3,17 +3,17 @@ import {_angleBetween, getAngleFromPoint} from '../helpers/helpers.math';
const TAU = Math.PI * 2;
function clipArc(ctx, model) {
const {startAngle, endAngle, pixelMargin, x, y} = model;
let angleMargin = pixelMargin / model.outerRadius;
function clipArc(ctx, element) {
const {startAngle, endAngle, pixelMargin, x, y, outerRadius, innerRadius} = element;
let angleMargin = pixelMargin / outerRadius;
// Draw an inner border by cliping the arc and drawing a double-width border
// Enlarge the clipping arc by 0.33 pixels to eliminate glitches between borders
ctx.beginPath();
ctx.arc(x, y, model.outerRadius, startAngle - angleMargin, endAngle + angleMargin);
if (model.innerRadius > pixelMargin) {
angleMargin = pixelMargin / model.innerRadius;
ctx.arc(x, y, model.innerRadius - pixelMargin, endAngle + angleMargin, startAngle - angleMargin, true);
ctx.arc(x, y, outerRadius, startAngle - angleMargin, endAngle + angleMargin);
if (innerRadius > pixelMargin) {
angleMargin = pixelMargin / innerRadius;
ctx.arc(x, y, innerRadius, endAngle + angleMargin, startAngle - angleMargin, true);
} else {
ctx.arc(x, y, pixelMargin, endAngle + Math.PI / 2, startAngle - Math.PI / 2);
}
@@ -22,60 +22,73 @@ function clipArc(ctx, model) {
}
function pathArc(ctx, model) {
function pathArc(ctx, element) {
const {x, y, startAngle, endAngle, pixelMargin} = element;
const outerRadius = Math.max(element.outerRadius - pixelMargin, 0);
const innerRadius = element.innerRadius + pixelMargin;
ctx.beginPath();
ctx.arc(model.x, model.y, model.outerRadius, model.startAngle, model.endAngle);
ctx.arc(model.x, model.y, model.innerRadius, model.endAngle, model.startAngle, true);
ctx.arc(x, y, outerRadius, startAngle, endAngle);
ctx.arc(x, y, innerRadius, endAngle, startAngle, true);
ctx.closePath();
}
function drawArc(ctx, model, circumference) {
if (model.fullCircles) {
model.endAngle = model.startAngle + TAU;
function drawArc(ctx, element) {
if (element.fullCircles) {
element.endAngle = element.startAngle + TAU;
pathArc(ctx, model);
pathArc(ctx, element);
for (let i = 0; i < model.fullCircles; ++i) {
for (let i = 0; i < element.fullCircles; ++i) {
ctx.fill();
}
model.endAngle = model.startAngle + circumference % TAU;
element.endAngle = element.startAngle + element.circumference % TAU;
}
pathArc(ctx, model);
pathArc(ctx, element);
ctx.fill();
}
function drawFullCircleBorders(ctx, element, model, inner) {
const endAngle = model.endAngle;
function drawFullCircleBorders(ctx, element, inner) {
const {x, y, startAngle, endAngle, pixelMargin} = element;
const outerRadius = Math.max(element.outerRadius - pixelMargin, 0);
const innerRadius = element.innerRadius + pixelMargin;
let i;
if (inner) {
model.endAngle = model.startAngle + TAU;
clipArc(ctx, model);
model.endAngle = endAngle;
if (model.endAngle === model.startAngle && model.fullCircles) {
model.endAngle += TAU;
model.fullCircles--;
element.endAngle = element.startAngle + TAU;
clipArc(ctx, element);
element.endAngle = endAngle;
if (element.endAngle === element.startAngle) {
element.endAngle += TAU;
element.fullCircles--;
}
}
ctx.beginPath();
ctx.arc(model.x, model.y, model.innerRadius, model.startAngle + TAU, model.startAngle, true);
for (i = 0; i < model.fullCircles; ++i) {
ctx.arc(x, y, innerRadius, startAngle + TAU, startAngle, true);
for (i = 0; i < element.fullCircles; ++i) {
ctx.stroke();
}
ctx.beginPath();
ctx.arc(model.x, model.y, element.outerRadius, model.startAngle, model.startAngle + TAU);
for (i = 0; i < model.fullCircles; ++i) {
ctx.arc(x, y, outerRadius, startAngle, startAngle + TAU);
for (i = 0; i < element.fullCircles; ++i) {
ctx.stroke();
}
}
function drawBorder(ctx, element, model) {
const options = element.options;
function drawBorder(ctx, element) {
const {x, y, startAngle, endAngle, pixelMargin, options} = element;
const outerRadius = element.outerRadius;
const innerRadius = element.innerRadius + pixelMargin;
const inner = options.borderAlign === 'inner';
if (!options.borderWidth) {
return;
}
if (inner) {
ctx.lineWidth = options.borderWidth * 2;
ctx.lineJoin = 'round';
@@ -84,17 +97,17 @@ function drawBorder(ctx, element, model) {
ctx.lineJoin = 'bevel';
}
if (model.fullCircles) {
drawFullCircleBorders(ctx, element, model, inner);
if (element.fullCircles) {
drawFullCircleBorders(ctx, element, inner);
}
if (inner) {
clipArc(ctx, model);
clipArc(ctx, element);
}
ctx.beginPath();
ctx.arc(model.x, model.y, element.outerRadius, model.startAngle, model.endAngle);
ctx.arc(model.x, model.y, model.innerRadius, model.endAngle, model.startAngle, true);
ctx.arc(x, y, outerRadius, startAngle, endAngle);
ctx.arc(x, y, innerRadius, endAngle, startAngle, true);
ctx.closePath();
ctx.stroke();
}
@@ -110,6 +123,8 @@ export default class Arc extends Element {
this.endAngle = undefined;
this.innerRadius = undefined;
this.outerRadius = undefined;
this.pixelMargin = 0;
this.fullCircles = 0;
if (cfg) {
Object.assign(this, cfg);
@@ -167,17 +182,9 @@ export default class Arc extends Element {
draw(ctx) {
const me = this;
const options = me.options;
const pixelMargin = (options.borderAlign === 'inner') ? 0.33 : 0;
const model = {
x: me.x,
y: me.y,
innerRadius: me.innerRadius,
outerRadius: Math.max(me.outerRadius - pixelMargin, 0),
pixelMargin,
startAngle: me.startAngle,
endAngle: me.endAngle,
fullCircles: Math.floor(me.circumference / TAU)
};
const offset = options.offset || 0;
me.pixelMargin = (options.borderAlign === 'inner') ? 0.33 : 0;
me.fullCircles = Math.floor(me.circumference / TAU);
if (me.circumference === 0) {
return;
@@ -185,14 +192,16 @@ export default class Arc extends Element {
ctx.save();
if (offset && me.circumference < TAU) {
const halfAngle = (me.startAngle + me.endAngle) / 2;
ctx.translate(Math.cos(halfAngle) * offset, Math.sin(halfAngle) * offset);
}
ctx.fillStyle = options.backgroundColor;
ctx.strokeStyle = options.borderColor;
drawArc(ctx, model, me.circumference);
if (options.borderWidth) {
drawBorder(ctx, me, model);
}
drawArc(ctx, me);
drawBorder(ctx, me);
ctx.restore();
}
@@ -206,7 +215,8 @@ Arc.id = 'arc';
Arc.defaults = {
borderAlign: 'center',
borderColor: '#fff',
borderWidth: 2
borderWidth: 2,
offset: 0
};
/**