Chartjs-Chartjs v4 pie chart, radial displacement (offset)

0👍

The javascript v4, uses ecmascript classes so custom controllers should be subclasses of existing ones, overwriting relevant methods, as summarly described in the "New Charts" section of the docs.

Thus, an analogous of the older code would be:

const PieController = Chart.controllers.pie;

class CutOutPie extends PieController{
    static id = 'cutOutPie';

    updateElement(arc, index, properties, mode) {
        const displacement = this.getDataset().displacements?.[index] || 0;
        if(displacement && properties.circumference){
            const angle = properties.startAngle + properties.circumference/2;
            properties.x += displacement * Math.cos(angle);
            properties.y += displacement * Math.sin(angle);
        }
        if(properties.outerRadius){
            properties.outerRadius -= Math.max(...this.chart.data.datasets[0].displacements);
        }
        super.updateElement(arc, index, properties, mode);
    }
}

Chart.register(CutOutPie);

new Chart('chart', {
    type: 'cutOutPie',
    data: {
        labels: ['a', 'b', 'c', 'd', 'e', 'f'],
        datasets: [{
            data: [1, 7, 2, 8, 3, 9],
            backgroundColor: ['red', 'orange', 'green', 'gold', 'pink', 'blue'],
            displacements: [0, 0, 40, 0, 0, 26],
        }]
    },
    options:{
        responsive: true,
        animation:{
            duration: 500,
            animateRotate: true,
            animateScale: true
        }
    }
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.1.2/chart.umd.js"
        integrity="sha512-t41WshQCxr9T3SWH3DBZoDnAT9gfVLtQS+NKO60fdAwScoB37rXtdxT/oKe986G0BFnP4mtGzXxuYpHrMoMJLA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

<body>
<div style="height:500px; width: 500px">
<canvas id="chart" style="border: 1px solid #999"></canvas>
</div>

The radii of the slices had to be scaled down in order to avoid displaced slices to extend outside of the canvas or overlap the legend or other elements.

Here’s a slightly more elaborate version, that precomputes the displacements and takes into consideration standard animations:

const PieController = Chart.registry.controllers.get('pie');
//this seems to be the most general way to get the controller class
// alternatives, depending on environment:
// PieController = Chart.PieController;
// PieController = Chart.controllers.pie;
// import {PieController} from 'chart.js';

class CutOutPie extends PieController{
    static id = 'cutOutPie';
    static defaults = {
        displacements: []
    }

    initialize(){
        super.initialize();
        this._displacementOffset = 0;
        this._offsetsValid = false;
        this._arcProperties = [];
    }

    updateElements(arcs, start, count, mode){
        this._offsetsValid = false;
        this._arcProperties = [];
        super.updateElements(arcs, start, count, mode);

        if(!this._offsetsValid){
            this._computeDisplacementOffsets(arcs);
            for(let i = start; i < start + count; i++){
                this.updateElement(arcs[i], i, {}, mode);
            }
        }
    }

    _computeDisplacementOffsets(arcs){
        if(Number.isFinite(this._arcProperties?.[0]?.outerRadius)){
            let startAngle = this._getRotation();
            for(let i = 0; i < arcs.length; i++){
                const displacement = this.getDataset().displacements?.[i] || 0;
                const endAngle = startAngle + this._circumference(i);
                this._arcProperties[i] = Object.assign(this._arcProperties[i] || {}, {dx: 0, dy: 0, shrink: 1});
                if(displacement){
                    const angle = (startAngle + endAngle)/2;
                    this._arcProperties[i].dx = Math.cos(angle) * displacement;
                    this._arcProperties[i].dy = Math.sin(angle) * displacement;
                    this._displacementOffset = Math.max(this._displacementOffset, displacement);
                }
                startAngle = endAngle;
            }

            for(let i = 0; i < arcs.length; i++){
                const outerRadius = this._arcProperties[i].outerRadius || this.outerRadius;
                if(outerRadius){
                    this._arcProperties[i].shrink =  (outerRadius - this._displacementOffset)/outerRadius ;
                }
            }
            this._offsetsValid = true;
        }
    }

    updateElement(arc, index, properties, mode) {
        this._arcProperties[index] = this._arcProperties[index] || {};
        Object.assign(this._arcProperties[index], properties || {});
        const animation = this.options.animation.animateRotate || this.options.animation.animateScale;
        if(this._offsetsValid){
            if(mode !== 'reset' || !animation){
                if(this._arcProperties[index].hasOwnProperty('dx')){
                    this._arcProperties[index].x += this._arcProperties[index].dx;
                }
                if(this._arcProperties[index].hasOwnProperty('dy')){
                    this._arcProperties[index].y += this._arcProperties[index].dy;
                }
                arc.x = this._arcProperties[index].x - this._arcProperties[index].dx;
                arc.y = this._arcProperties[index].y - this._arcProperties[index].dy;
            }
            const shrink = this._arcProperties[index].shrink;
            if(shrink){
                this._arcProperties[index].outerRadius *= shrink;
                this._arcProperties[index].innerRadius *= shrink;
            }
            super.updateElement(arc, index, this._arcProperties[index], mode);
        }
    }
}

Chart.register(CutOutPie);

new Chart('chart', {
    type: 'cutOutPie',
    data: {
        labels: ['a', 'b', 'c', 'd', 'e', 'f'],
        datasets: [{
            data: [1, 7, 2, 8, 3, 9],
            backgroundColor: ['red', 'orange', 'green', 'gold', 'pink', 'blue'],
            displacements: [0, 0, 40, 0, 0, 26],
        }]
    },
    options:{
        responsive: true,
        animation:{
            duration: 500,
            animateRotate: true,
            animateScale: true
        }
    }
});
<div style="height:500px; width: 500px">
<canvas id="chart" style="border: 1px solid #999"></canvas>
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.1.2/chart.umd.js"
        integrity="sha512-t41WshQCxr9T3SWH3DBZoDnAT9gfVLtQS+NKO60fdAwScoB37rXtdxT/oKe986G0BFnP4mtGzXxuYpHrMoMJLA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

Leave a comment