In this post I will provide my implementation of creating a custom block plugin pie chart for use in WordPress.
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.
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.Β
{
"$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
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
<>
<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>
</>
