mirror of
https://github.com/chartjs/Chart.js.git
synced 2026-03-06 08:24:05 +01:00
Add offset option for arc (#7691)
* Add offset option for arc * Finishing touches
This commit is contained in:
committed by
Evert Timberg
parent
8fadae6c91
commit
483d06eebd
@@ -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.
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -148,9 +148,7 @@ PolarAreaController.defaults = {
|
||||
'borderColor',
|
||||
'borderWidth',
|
||||
'borderAlign',
|
||||
'hoverBackgroundColor',
|
||||
'hoverBorderColor',
|
||||
'hoverBorderWidth'
|
||||
'offset'
|
||||
],
|
||||
|
||||
animation: {
|
||||
|
||||
@@ -714,7 +714,7 @@ export default class DatasetController {
|
||||
|
||||
/**
|
||||
* @param {number} index
|
||||
* @param {string} mode
|
||||
* @param {string} [mode]
|
||||
* @protected
|
||||
*/
|
||||
resolveDataElementOptions(index, mode) {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user