Create a Block Plugin Pie Chart For Use In WordPress
Create a custom WordPress block plugin pie chart utilizing data array attributes to update the UI of your pages.


In this post I will provide my implementation of creating a custom block plugin pie chart for use in WordPress.

NOTE: This has not been tested as responsive for mobile. Sizing and layout will need to be adjusted if adding mobile support.

Awesome Pie Chart

  • _ One
  • _ Two
  • _ Three


This pie chart is derived from a previous post about using Javascript to create a pie chart in the link below.

I have ported this to a WordPress block plugin so the data attribute can be edited and updated dynamically.

The Javascript file used to house the pie chart has been built as a bundled module using Webpack and Babel.

View this post below on the steps taken to achieve bundling a custom Javascript class.

Generally, when the source is built using Webpack and Babel, the source is not readable. For this reason, in the below example of my custom block plugin pie chart, I am posting the Javascript source which isn't built using Webpack.Β Β 

Custom Pie Chart Block Plugin

We will start this example after initial setup of a template WordPress block plugin. You can learn how to set this up from a previous post I have created from the link below.

Editing WordPress Block PluginΒ 

Custom Javascript Pie ChartΒ 

I will post a breakdown of my custom Javascript pie chart class here so you can view it. It has been slightly edited from the post in the link above to include dynamic data processing for this block plugin pie chart in WordPress.

Breakdown

Pie Chart Class And Constructor

First we create the class and create variables that the class will use in its instance. The parameters passed to the class are the DOM refsΒ used to find the container elements along with the data array that is built in the editor.

Finally, at the bottom of the class, we specify that it is an exported module.

class MyPieChart {

	constructor(userInteractDiv, pieChartCanvas, listRef, data){
		this.userInteractDiv = userInteractDiv;
		this.pieChartCanvas = pieChartCanvas;
    this.listRef = listRef;
		this.data = data;
		this.padding = 15;

		this.total = 0;
		this.mouseAngle = -1;
		this.initAnimValues = {index: 0, angle: 0};
	}
	
	//add functions here
	...
	
	if (typeof module !== 'undefined' && module.exports) {
	module.exports = {
		MyPieChart
	};
	
	}
init()

This function will add event listeners to the DOM CanvasΒ element. Also, it processes the data array to build a list that is posted on the right of the pie chart as well as calculating the total for the sum of the data values.

	init(){
 		this.pieChartCanvas.addEventListener(
			"mousemove",
			(event) => {
				this.mouseAngle = this.calcAngleFromMouse(event.offsetY, event.offsetX, this.pieChartCanvas.clientWidth/2);
			},
			false,
		);
		this.pieChartCanvas.addEventListener(
			"mouseout",
			(event) => {
				this.mouseAngle = -1;
			},
			false,
		);

		this.listRef.innerHTML = "";

		for(let j = 0; j < this.data.length; j++){
			var li = `<li><span style="background-color: ${this.data[j].color};"> __ </span><span>${this.data[j].value} - ${this.data[j].text}</span></li>`;
			this.listRef.innerHTML += li;
			this.total += parseInt(this.data[j].value, 10);
		}
		requestAnimationFrame(this.initAnim.bind(this));
	}
initAnim()

Generally, this starts the initial animation. The initial animation will go through each degree from 0 to 360 and add the data accordingly.

    initAnim(){
		if(this.initAnimValues.index === this.data.length){
			requestAnimationFrame(this.chartAnim.bind(this));
		} else {
			requestAnimationFrame(this.initAnim.bind(this));
			//setTimeout(this.initAnim.bind(this), 1);
		}
		var lastAngle = 0;

		var chart = this.pieChartCanvas;
  	var canvas = chart.getContext('2d');
  	
  	//add below line for sizing on mobile devices along with CSS changes
  	canvas.height = window.innerWidth;
  	
		const center = {x: chart.width/2, y: chart.height/2};

  		canvas.clearRect(0, 0, chart.width, chart.height);
		canvas.shadowColor = "#000000";
		canvas.shadowOffsetX = 1;
		canvas.shadowOffsetY = 1;
		
		for(let i = 0; i < this.data.length; i++){
			if(i > this.initAnimValues.index){
				break;
			}
			canvas.beginPath();
			canvas.strokeStyle = "#000000";
			canvas.lineWidth = 1;
			var currentAngle = this.calcPercent(this.data[i].value);
			canvas.fillStyle = this.data[i].color;
			if(this.initAnimValues.index === i && this.initAnimValues.angle < currentAngle){
				canvas.arc(center.x, center.y, center.x - this.padding * 4, this.degreesToRadians(lastAngle), this.degreesToRadians(this.initAnimValues.angle + lastAngle), false);
				this.initAnimValues.angle+=5;
				if(this.initAnimValues.angle >= currentAngle){
					this.initAnimValues.index++;
					this.initAnimValues.angle = 0;
				}
			} else {
				canvas.arc(center.x, center.y, center.x - this.padding * 4, this.degreesToRadians(lastAngle), this.degreesToRadians(currentAngle + lastAngle), false);
			}
			canvas.lineTo(center.x, center.y);
			canvas.closePath();
			canvas.fill();
			lastAngle+= currentAngle;
		}

	}
chartAnim()

Finally, the last animation call that will animate each pie slice out of the center of the circle a little when the user interacts with it.

    chartAnim(){
		var lastAngle = 0;

		var chart = this.pieChartCanvas;
  		var canvas = chart.getContext('2d');

		const center = {x: chart.width/2, y: chart.height/2};
		
  		canvas.clearRect(0, 0, chart.width, chart.height);
		canvas.shadowColor = "#000000";
		canvas.shadowOffsetX = 1;
		canvas.shadowOffsetY = 1;
		for(let i = 0; i < this.data.length; i++){
			canvas.beginPath();
			canvas.strokeStyle = "#000000";
			canvas.lineWidth = 1;
			var currentAngle = this.calcPercent(this.data[i].value);
			if(this.mouseAngle > lastAngle && this.mouseAngle < (currentAngle + lastAngle)){

				var offsetDegrees = (currentAngle / 2) + lastAngle;
				var offset = this.getPointOnCircle(center.x, center.y, this.padding * 2, offsetDegrees);
				var textOffset = this.getPointOnCircle(center.x, center.y, center.x/2, offsetDegrees);

				canvas.fillStyle = this.data[i].color;
				canvas.moveTo(offset.x, offset.y);
				canvas.arc(offset.x, offset.y, center.x - this.padding * 4, this.degreesToRadians(lastAngle), this.degreesToRadians(currentAngle + lastAngle), false);
				canvas.lineTo(offset.x, offset.y);

				canvas.closePath();
				canvas.stroke();
				canvas.fill();
			} else {
				canvas.fillStyle = this.data[i].color;
				canvas.arc(center.x, center.y, center.x - this.padding * 4, this.degreesToRadians(lastAngle), this.degreesToRadians(currentAngle + lastAngle), false);
				canvas.lineTo(center.x, center.y);

				canvas.closePath();

				canvas.fill();
			}
			
			lastAngle+= currentAngle;
		}
		lastAngle = 0;
		canvas.save();
		for(let i = 0; i < this.data.length; i++){

			var currentAngle = this.calcPercent(this.data[i].value);

			if(this.mouseAngle > lastAngle && this.mouseAngle < (currentAngle + lastAngle)){
				canvas.shadowColor = "#000000";
				canvas.shadowOffsetX = 2;
				canvas.shadowOffsetY = 2;
				canvas.fillStyle = "#ffffff";
				canvas.font = "30px Serif";
				canvas.fillText(this.data[i].text, textOffset.x, textOffset.y);
			}
			lastAngle+= currentAngle;
		}
		canvas.restore();

        requestAnimationFrame(this.chartAnim.bind(this));
		
	}
calcPercent(value)

Basically, math calculation used to find the total degree of data value will occupy.

	calcPercent(value){
		var newVal = 360.0 * (value / this.total);
		return newVal;
	}
getPointOnCircle(centerX, centerY, radius, angleInDegrees)

Generally, this calculates the offset of the text to display on the canvas as well as the offset to slide the pie slide out on interaction.

	getPointOnCircle(centerX, centerY, radius, angleInDegrees) {
		const angleInRadians = this.degreesToRadians(angleInDegrees);
		const x = centerX + radius * Math.cos(angleInRadians);
		const y = centerY + radius * Math.sin(angleInRadians);
		return { x, y };
	}
calcAngleFromMouse(y, x, center)

Basically finds the degree at which the user interaction is occurring.

	calcAngleFromMouse(y, x, center){
		var newVal = Math.atan2(y-center, x-center) * 180 / Math.PI;
		if(newVal < 0){
			newVal = 180 - Math.abs(newVal) + 180;
		}
		return newVal;
	}
degreesToRadians(degrees)

Gets the radians from a degree value.

	degreesToRadians(degrees) {
		return degrees * (Math.PI / 180);
	}
radiansToDegrees(radians)

Gets the degrees from a radian value.

	radiansToDegrees (radians) {
   		return radians * (180/Math.PI);
	}

Block.jsonΒ 

Generally, the block.json file holds what and how WordPress accesses the data attributes. So we will need to add a few data attributes to the block.jsonΒ file.Β 

This is the style background color for the plugin.

The title for the pie chart.

The array of data for the pie chart to process.

{
	"$schema": "https://schemas.wp.org/trunk/block.json",
	"apiVersion": 3,
	"name": "r2creations24/my-custom-blocks-mypiechart",
	"version": "0.1.0",
	"title": "My Custom Blocks My Pie Chart",
	"category": "widgets",
	"icon": "smiley",
	"description": "Custom blocks created by R2creations24.",
	"example": {},
	"attributes": {
		"bgColor": {
			"type": "string",
			"default": "#219b8b"
		},
		"title": {
			"type": "string",
			"default": "My Pie Chart"
		},
		"list": {
			"type": "array",
			"default":[{"value":"100","text":"Sample 1","color": "#ff00ff"},     {"value":"100","text":"Sample 2","color": "#00ffff"}],
			"query": {
				"value":{
					"type":"string",
					"source": "text"
				},
				"text":{
					"type":"string",
					"source": "text"
				},
				"color":{
					"type":"string",
					"source": "text"
				}
			}
		}
	},
	"supports": {
		"html": false
	},
	"textdomain": "my-custom-blocks-mypiechart",
	"editorScript": "file:./index.js",
	"editorStyle": "file:./index.css",
	"render": "file:./render.php",
	"style": "file:./style-index.css",
	"viewScript": "file:./view.js"
}

Styling the UIΒ 

Then we will style the UI using the style.scssΒ  of the block plugin. Using this stylesheet, the UI will update on the frontend and the backend.

.wp-block-r2creations24-my-custom-blocks-mypiechart {
	background-color: var(--bg-color);
	color: #fff;
	padding: 2px;
    display: block;
    align-items: center;
}

.wp-block-r2creations24-my-custom-blocks-mypiechart .header {
    margin: 0;
    text-align: center;
    padding-top: 15px;
    text-shadow: 3px 3px black;
    color: white
}

.wp-block-r2creations24-my-custom-blocks-mypiechart .center {
    display: inline-flex;
    align-items: center;
    width: 100%;
    margin: auto;
    justify-content: space-evenly;
}

.wp-block-r2creations24-my-custom-blocks-mypiechart #piechartTest {
    width: 50%;
    display: inline;
    aspect-ratio: 1 / 1;
}

.wp-block-r2creations24-my-custom-blocks-mypiechart .center ul {
	  list-style: none;
    width: 40%;
    margin: 0;
    padding: 0;
}

.wp-block-r2creations24-my-custom-blocks-mypiechart .center ul li {
    padding: 5px 0 20px 0;
    font-size: 12px;
}

.wp-block-r2creations24-my-custom-blocks-mypiechart .center ul li span:first-child {
    background-color: red;
    color: rgba(255, 255, 255, 0);
    box-shadow: 2px 2px black;
    padding: 5px;
    margin: 5px;
}

.wp-block-r2creations24-my-custom-blocks-mypiechart .center ul li span:last-child {
	align-content: center;
}

Creating the Frontend

Since we are building this using dynamic rendering, we will use render.phpΒ to build the frontend UI. The attributes are retrieved into a PHP variable and filled into the DOM appropriately. When passing the data array along from PHP to Javascript, you need to use json_encode()Β function.

<?php

 $bgColor = $attributes['bgColor'];
 $data = $attributes['list'];
 $title = $attributes['title'];

 $style = "--bg-color: ".$bgColor.";";
?>

<div <?php echo get_block_wrapper_attributes(array("style" => $style)); ?>>
    <div><h1><?php echo $title; ?></h1></div>
        <div class="center">
            <canvas id="piechartTest" width="450px" height="450px" ></canvas>
            <ul id="list">
                <li><span> _ </span>One</li>
                <li><span> _ </span>Two</li>
                <li><span> _ </span>Three</li>
            </ul>
        </div>
</div>
<script>

    var centerDiv = document.getElementsByClassName("center")[0];
    var piechart = document.getElementById("piechartTest");
    var list = document.getElementById("list");
    var data = <?php echo json_encode($data); ?>;
    var myPieChart = new MyPieChart(
        centerDiv, 
        piechart, 
        list,
        data
    );

    myPieChart.init();
 
</script>

WordPress Hook To Add Custom Javascript File

Then we will need to add a hook to import the custom Javascript file to include in our frontend. This is done by editing your PHP file off the root of your plugin directory.

At the bottom of the file, I have added the below code.Β 

function r2creations24_my_custom_blocks_enqueue_script()
{   
	wp_enqueue_script( 'r2creations24_my_custom_blocks_script_mypiechart', plugin_dir_url( __FILE__ ) . 'assets/scripts/piechart.js' );
}

add_action('wp_enqueue_scripts', 'r2creations24_my_custom_blocks_enqueue_script');

Building the EditorΒ 

Finally we just need to build the editor UI to display the pie chart and update the data.

In the edit.jsΒ file of this block plugin pie chart, we need to use the useRef and useEffectΒ hooks in WordPress so we can access the DOM elements.

Imports

import { __ } from '@wordpress/i18n';

import { 
	useBlockProps,
	PanelColorSettings,
	InspectorControls
 		} from '@wordpress/block-editor';

import { 
    PanelBody,
    CardBody,
    TextControl,
    __experimentalNumberControl as NumberControl,
    Button,
    PanelRow,
    BaseControl
        } from '@wordpress/components';

import './editor.scss';

import { useRef, useEffect } from '@wordpress/element';
import { MyPieChart } from '../../assets/scripts/piechart.js';

export default function Edit( { attributes, setAttributes } ) {

//add constants here
...

//add functions here
...

  return (

  ...

  );
}

Functions Inside Edit Function

Add Constants
Before moving forward, we need to generate const values.
  const pieChartRef = useRef();
  const centerRef = useRef();
  const listRef = useRef();

	const newStyle = {
        "--bg-color": attributes.bgColor
    };

  const { list } = attributes;
    
  const blockProps = useBlockProps( { style: newStyle } );

... //functions

    useEffect(() => {
        
        var myPieChart = new MyPieChart(centerRef.current, pieChartRef.current, listRef.current, attributes.list);
        myPieChart.init();

    }, [attributes.list]);
setBgColor(value)

Set background color to new value for the block plugin.

	function setBgColor(value){
        if(value === undefined){
            setAttributes({bgColor: "#219b8b"});
        } else {
             setAttributes({bgColor: value});
        }
    }
randomColor()

Generally used when creating a new entry in the data.

  function randomColor(){
		return Math.floor(Math.random() * 0xffffff).toString(16);
	}
changeColor(index, value, attributes)
    function changeColor(index, value, attributes){
        if(value === undefined){
            value = "#" + randomColor();
        }
        var newList = [...list];
        var arr = newList[index];
        arr.color = value;
        newList[index] = arr;
        setAttributes({list: newList});
        console.log(attributes.list);
    }
changeText(index, value, attributes)
    function changeText(index, value, attributes){
        var newList = [...list];
        var arr = newList[index];
        arr.text = value;
        newList[index] = arr;
        //newList[index] = {text: value, color: newList[index].color};
        setAttributes({list: newList});
        console.log(attributes.list);
    }
changeValue(index, value, attributes)
    function changeValue(index, value, attributes){
        if(value === undefined || value.length === 0){
            value = 0;
        }
        var newList = [...list];
        var arr = newList[index];
        arr.value = value;
        newList[index] = arr;
        //newList[index] = {text: value, color: newList[index].color};
        setAttributes({list: newList});
        console.log(attributes.list);
    }
addMoreToData()
    function addMoreToData(){
        var newList = [...list];
        var arr = {value: 100, color: "#" + randomColor(), text: "Sample"};
        newList.push(arr);
        setAttributes({list: newList});
        console.log(attributes.list);
    }

Creates a new list entry to the end of the data attribute.

removeLastData()
    function removeLastData(){
        var newList = [...list];
        setAttributes({list: newList.slice(0, -1)});
        console.log(attributes.list);
    }

Removes the last entry from the data array attribute.

Return UI In Edit Function

Finally just need to render the editor UI.
		
		<>
		    <InspectorControls>
                <PanelBody title={ __( 'Settings', 'my-custom-blocks-mypiechart' ) }>
                    <PanelColorSettings
                        title = { __('Configure Color')}
                        colorSettings={
                            [
                                {
                                    value: attributes.bgColor,
                                    onChange: (value) => setBgColor(value),
                                    label: __('Background Color')
                                }
                            ]
                        }
                    /> 
                </PanelBody>
                <PanelBody title={ __( 'Data Input', 'my-custom-blocks-mypiechart' ) }>
                    <BaseControl __nextHasNoMarginBottom>
                        <BaseControl.VisualLabel>Chart Title</BaseControl.VisualLabel>
                            <TextControl
                                __nextHasNoMarginBottom
                                __next40pxDefaultSize
                                value= {attributes.title}
                                onChange={(value) => setAttributes({title : value})}
                                placeholder={ __( 'Edit Title Name' ) } 
                            />
                    </BaseControl>
                    <PanelRow>
                        <Button
                            variant="primary"
                            onClick={  addMoreToData } >
                            Add More To Data
                        </Button>
                   </PanelRow>
                    <PanelRow>
                        <Button
                            variant="primary"
                            onClick={  removeLastData } >
                            Remove Last Entry
                        </Button>
                   </PanelRow>
                </PanelBody>
                    { attributes.list.map((item, index) => {
                        return (
                            <PanelBody 
                            level={ 4 }
                            title= { `Entry: ${index + 1}` }>
                                <CardBody>
                                    <BaseControl __nextHasNoMarginBottom>
                                    <BaseControl.VisualLabel>Name</BaseControl.VisualLabel>
                                        <TextControl
                                            __nextHasNoMarginBottom
                                            __next40pxDefaultSize
                                            value= {item.text}
                                            onChange={(value) => changeText(index, value, attributes)}
                                            placeholder={ __( 'Edit Data Text' ) } 
                                        />
                                    </BaseControl>
                                    <BaseControl __nextHasNoMarginBottom>
                                        <BaseControl.VisualLabel>Value</BaseControl.VisualLabel>
                                        <NumberControl 
                                            __next40pxDefaultSize
                                            spinControls='native'
                                            min={ 0 }
                                            value={ item.value }
                                            onChange={ (value) => changeValue(index, value, attributes) }
                                        />
                                    </BaseControl>
                                    <PanelColorSettings
                                        colorSettings={
                                            [
                                                {
                                                    value: item.color,
                                                    onChange: (value) => changeColor(index, value, attributes),
                                                    label: __('Data Color')
                                                }
                                            ]
                                        }
                                    /> 
                                </CardBody>
                            </PanelBody>
                            )
                        }
                    )}
            </InspectorControls>
		<div { ...blockProps }>
            <div><h1>{attributes.title}</h1></div>
            <div class="center" ref={ centerRef }>
                <canvas ref={ pieChartRef } id="piechartTest" width="450px" height="450px" ></canvas>
                <ul id="list" ref={ listRef }>
                    <li><span> _ </span>One</li>
                    <li><span> _ </span>Two</li>
                    <li><span> _ </span>Three</li>
                </ul>
            </div>
        </div>
		</>
		
Hope this was helpful in creating a block plugin pie chart for use in WordPress.