When we had a client request come in for a pie chart that used ChartJS, we were sure that the WordPress repo would have an existing block plugin that would generate a very simple pie chart without any fuss. Sometimes there’s gaps in the community though and this, apparently, was one of them. All the choices had either too many options, not enough options, were no longer supported, had really terrible UI, or were “freemium” and would clutter the dashboard with ads. Sure, these days an LLM could generate a decent enough boilerplate that might get us 80% there, but the real success lies in that last 20%. So, we turned to the idea of just writing it ourselves and documenting it at the same time, in the hopes that it will serve as a decent tutorial for anybody looking for a practical guide to understanding how to approach custom blocks that integrate with other libraries.
Note: this tutorial is going to assume you have experience with React, and ideally have also looked at the anatomy of a custom block.
While we will start with a basic first pass iteration, the second half of the post will involve heavy refactoring, so be sure to check that out before utilizing any code examples.
Here’s what we’re going to be creating:
Set up our development environment
By this point, building Custom Blocks for WordPress has grown significantly in popularity and writing our own blocks for all sorts of unique purposes has been streamlined greatly with WordPress’ create-block package. You’ll want to run this command and get your code editor set up with this basic starter block:
Install our Dependencies
We know we’re going to need to use ChartJS, as it’s arguably the best library out there for generating custom charts from datasets. We’re also going to need to use the React ChartJS package for integration with our React editor experience.
npm install chart.js@^4.4.2 chartjs-plugin-datalabels@^2.2.0 react-chartjs-2@^5.2.0
Generating the Backend
First we’re going to get the block functional and editable in the Block Editor, and once we have some data loaded, we’ll pivot to the frontend to display the chart to the user. To tackle the backend, we’re going to need to do the following:
- Setting up our Block Attributes (can be thought of as “custom fields” of sorts)
- Importing the ChartJS library, as well as the Block Editor components
- Configuring ChartJS and setting up the data structure
- Writing the functions for the individual slices and their attributes
- Writing the functions for the addition and removal of individual slices
Set up our Block Attributes
We know we’re going to need some fields to handle the attributes of the slices; title, color, values and percentage. Let’s start with those and add the following attributes
to block.json, along with some starter values.
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "chee-block/wp-simple-pie-chart-block",
"version": "1.0",
"title": "Simple Pie Chart",
"category": "text",
"icon": "chart-pie",
"description": "A block for creating simple and easy pie charts",
"supports": {
"html": false
},
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css",
"textdomain": "chee-blocks",
"attributes": {
"slices": {
"type": "array",
"default": [
{
"sliceTitle": "",
"sliceColor": "#666",
"slicePercentage": 10,
"sliceValue": ""
}
]
}
}
}
Setting up Imports
Let’s start with some imports that we know we’re going to need if we’re going to be working with ChartJS at all, particularly the Pie Chart:
import { __ } from '@wordpress/i18n';
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { Chart as ChartJS, ArcElement, Tooltip, Legend } from 'chart.js';
import { Pie } from 'react-chartjs-2';
import ChartDataLabels from 'chartjs-plugin-datalabels';
import './editor.scss';
export default function Edit({ attributes, setAttributes }) {
const blockProps = useBlockProps();
return (
<div className="pie-chart" {...blockProps}>
</div>
);
}
Chart Options
We’ll start with some basic options. Part of this project is a custom legend, so we’re going to turn off the default ChartJS legend.
const chartOptions = {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: {
display: false,
}
}
};
Setting up the Data
In our block.json, we created an attribute called slices, which was configured to be an array, so let’s import those attributes. Then we’ll map them to the ChartJS data structure that we need:
const { slices } = attributes;
const sliceData = (slices) => ({
labels: slices.map(slice => slice.sliceTitle),
datasets: [{
data: slices.map(slice => slice.slicePercentage),
backgroundColor: slices.map(slice => slice.sliceColor),
value: slices.map(slice => slice.sliceValue),
hoverOffset: 4 // optional
}]
});
Slice Attributes
Each slice is going to come along with a variety of attributes we need to update. The best way to approach this is to leverage the Block Editor’s InspectorControls and place these elements in the sidebar. We have a few attributes we need to be able to manipulate; text inputs, a color selector, and a numerical value for the percentage. For these attributes, we’ll import TextControl, RangeControl, ColorPalette, and we’ll also import Button since we’ll likely need that, as well. Since there will be multiple slices for every Chart, we need a new component to loop over so let’s create that next.
Before we do that, let’s set up some handler functions to set the values of each slice. We’ll get the ID of the slice we’re editing, return the slice along with the rest of the others in the array, and finally update our slices
attribute array with the updated slices:
const handleTitleChange = (id, newTitle) => {
const updatedSlices = slices.map((slice, index) => {
if (index === id) {
return { ...slice, sliceTitle: newTitle };
}
return slice;
});
setAttributes({ slices: updatedSlices });
};
const handleColorChange = (id, color) => {
const updatedSlices = slices.map((slice, index) => {
if (index === id) {
return { ...slice, sliceColor: color };
}
return slice;
});
setAttributes({ slices: updatedSlices });
};
const handleValueChange = (id, newValue) => {
const updatedSlices = slices.map((slice, index) => {
if (index === id) {
return { ...slice, sliceValue: newValue };
}
return slice;
});
setAttributes({ slices: updatedSlices });
};
const handlePercentageChange = (id, newFunds) => {
const updatedSlices = slices.map((slice, index) => {
if (index === id) {
return { ...slice, slicePercentage: newFunds };
}
return slice;
});
setAttributes({ slices: updatedSlices });
};
const handleRemoveSlice = (id) => {
const updatedSlices = slices.filter((_, index) => index !== id);
setAttributes({ slices: updatedSlices });
};
And the SliceEntry component. Note that I’ve added some additional items such as formatting of the inputs and piping in the theme’s global color palette as some “quality of life” features.
const SliceEntry = ({ id, slice, onTitleChange, onPercentageChange, onRemoveSlice, onColorChange, onValueChange }) => {
const colorPalette = wp.data.select("core/block-editor").getSettings().colors;
return (
<>
<div id={id} style={{ display: "flex", alignItems: "center", gap: "10px" }}>
<TextControl
label="Slice Title"
value={slice.sliceTitle}
onChange={(value) => onTitleChange(id, value)}
/>
<TextControl
label="Slice Value"
value={slice.sliceValue}
onChange={(value) => onValueChange(id, value)}
/>
<Button
onClick={() => onRemoveSlice(id)}>
<Icon
icon="remove"
/>
</Button>
</div>
<RangeControl
label="Percentage Value"
value={slice.slicePercentage}
onChange={(value) => onPercentageChange(id, value)}
min={0}
max={100}
step={0.1}
/>
<ColorPalette
label="Slice Color"
colors={colorPalette}
value={slice.sliceColor}
onChange={(color) => onColorChange(id, color)}
/>
</>
)
};
Now let’s put it all together! Note that the SliceEntry function/component is defined outside of the main edit.js
function to ensure we don’t get unnecessary re-renders when a slice is updated/added/removed. We also included our add/remove slice functions, which we’ll define and hook up next:
import { __ } from '@wordpress/i18n';
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { TextControl, RangeControl, ColorPalette, Button, Icon } from '@wordpress/components';
import { Chart as ChartJS, ArcElement, Tooltip, Legend } from 'chart.js';
import { Pie } from 'react-chartjs-2';
import ChartDataLabels from 'chartjs-plugin-datalabels';
import './editor.scss';
const SliceEntry = ({ id, slice, onTitleChange, onPercentageChange, onRemoveSlice, onColorChange, onValueChange }) => {
const colorPalette = wp.data.select("core/block-editor").getSettings().colors;
return (
<>
<div id={id} style={{ display: "flex", alignItems: "center", gap: "10px" }}>
<TextControl
label="Slice Title"
value={slice.sliceTitle}
onChange={(value) => onTitleChange(id, value)}
/>
<TextControl
label="Slice Value"
value={slice.sliceValue}
onChange={(value) => onValueChange(id, value)}
/>
<Button
onClick={() => onRemoveSlice(id)}>
<Icon
icon="remove"
/>
</Button>
</div>
<RangeControl
label="Percentage Value"
value={slice.slicePercentage}
onChange={(value) => onPercentageChange(id, value)}
min={0}
max={100}
step={0.1}
/>
<ColorPalette
label="Slice Color"
colors={colorPalette}
value={slice.sliceColor}
onChange={(color) => onColorChange(id, color)}
/>
</>
)
};
export default function Edit({ attributes, setAttributes }) {
const blockProps = useBlockProps();
ChartJS.register(
ArcElement,
Tooltip,
Legend
);
const { slices } = attributes;
const sliceData = (slices) => ({
labels: slices.map(slice => slice.sliceTitle),
datasets: [{
data: slices.map(slice => slice.slicePercentage),
backgroundColor: slices.map(slice => slice.sliceColor),
value: slices.map(slice => slice.sliceValue),
hoverOffset: 4
}]
});
const handleTitleChange = (id, newTitle) => {
const updatedSlices = slices.map((slice, index) => {
if (index === id) {
return { ...slice, sliceTitle: newTitle };
}
return slice;
});
setAttributes({ slices: updatedSlices });
};
const handleColorChange = (id, color) => {
const updatedSlices = slices.map((slice, index) => {
if (index === id) {
return { ...slice, sliceColor: color };
}
return slice;
});
setAttributes({ slices: updatedSlices });
};
const handleValueChange = (id, newValue) => {
const updatedSlices = slices.map((slice, index) => {
if (index === id) {
return { ...slice, sliceValue: newValue };
}
return slice;
});
setAttributes({ slices: updatedSlices });
};
const handlePercentageChange = (id, newPercentage) => {
const updatedSlices = slices.map((slice, index) => {
if (index === id) {
return { ...slice, slicePercentage: newPercentage };
}
return slice;
});
setAttributes({ slices: updatedSlices });
};
return (
<div className="pie-chart" {...blockProps}>
<Pie
options={chartOptions}
data={data}
plugins={[ChartDataLabels]}
/>
<InspectorControls>
<Button
variant="primary"
onClick={handleAddSlice}>
Add Slice
</Button>
{slices.map((slice, index) => (
<SliceEntry
key={index}
id={index}
slice={slice}
onTitleChange={handleTitleChange}
onColorChange={handleColorChange}
onPercentageChange={handlePercentageChange}
onValueChange={handleValueChange}
onRemoveSlice={onRemoveSlice}
/>
))}
</InspectorControls>
</div>
);
}
Adding/Removing Slices
The user needs the ability to add a new slice and remove one as needed. We’ll define these functions, following the similar convention we used before:
const handleAddSlice = () => {
const updatedSlices = [{ sliceTitle: 'New Slice', sliceColor: '#444', slicePercentage: 10, sliceValue: '1,000' }, ...slices];
setAttributes({ slices: updatedSlices });
};
const onRemoveSlice = (id) => {
const updatedSlices = slices.filter((_, index) => index !== id);
setAttributes({ slices: updatedSlices });
};
And here we are:
Generating the Frontend
Currently this block is only outputting to the editor, but has no frontend for the user to see the pie chart itself. To tackle this, we’re going to need a few things:
- PHP file for the render template
- Frontend script that will initialize ChartJS with all our data
PHP Template
There’s two ways to display a block on the frontend: One way is the save.js
function that will store the data of the block within the database and return to the user as HTML when the block is saved (called Static Rendering). The save.js
function cannot include any dynamic calls to a store or database, so this works great for blocks that don’t have any dynamic data or frequent changes, as the rendered markup will always be consistent (even if the attributes are changing).
The other type of block rendering is dynamic blocks, where the rendering is done server-side and can contain markup and elements that change frequently. A common use for these types of blocks would be something like a Recent Posts Feed, which would rely on database calls to populate the block. In our case, the ChartJS script relies on generating a dynamic canvas element on the fly, which is not something we can save to the database, so we will go down the route of server-side rendering via a dynamic block.
To do that, we will first assign a render template in the block.json:
{
...
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css",
"render": "file:./render.php", <- add this
...
}
For our render.php
, we’ll place the following. This is standard boilerplate, but the key thing to note is how attributes get accessed (via $attributes
and that we’re passing in the Slices data via a data-
attribute within the HTML.
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
$block_props = get_block_wrapper_attributes([
'class' => 'wp-simple-pie-chart-block',
]);
$slices = isset($attributes['slices']) ? $attributes['slices'] : [];
$dataSlices = esc_attr(json_encode($slices));
?>
<div <?= $block_props; ?>>
<div class="wp-simple-pie-chart-block__display">
<div class="wp-simple-pie-chart-block__init wp-simple-pie-chart-block-instance" data-slices="<?= $dataSlices; ?>">
</div>
</div>
</div>
Initialize ChartJS on Frontend
Now that we have our PHP render template set up, the last step is to enqueue and initialize it on the frontend so it will render the chart! To do so, we need to first enqueue a frontend javascript via the block.json
file:
{
...
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css",
"render": "file:./render.php",
"viewScript": "file:./view.js", <- add this
...
}
Now let’s work on our frontend JS file. Note that we are importing the ChartJS library again, as well as importing our slices data:
import { Chart as ChartJS, PieController, ArcElement, Tooltip, Legend, DoughnutController } from 'chart.js';
import ChartDataLabels from 'chartjs-plugin-datalabels';
ChartJS.register(ArcElement, Tooltip, Legend, PieController);
const sliceData = (slices) => ({
labels: slices.map(slice => slice.sliceTitle),
datasets: [{
data: slices.map(slice => slice.slicePercentage),
backgroundColor: slices.map(slice => slice.sliceColor),
value: slices.map(slice => slice.sliceValue),
hoverOffset: 4
}]
});
const chartOptions = {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: { display: false }
}
};
// Chart Options
const initChart = (chart) => {
const canvas = document.createElement('canvas');
// get the Slices from our data in our render.php
const slices = JSON.parse(chart.dataset.slices);
// map the slices data to the ChartJS data structure defined in sliceData()
const data = sliceData(slices);
const chartInstance = new ChartJS(canvas, {
type: 'pie',
data: data,
options: chartOptions,
plugins: [ChartDataLabels],
});
chart.appendChild(canvas);
};
// Init Charts
document.addEventListener('DOMContentLoaded', () => {
const charts = document.querySelectorAll('.wp-simple-pie-chart-block-instance');
charts.forEach(chart => {
initChart(chart);
});
});
At this point, we have a fully functional Pie Chart Block in the backend, and rendering on the frontend:
And that’s a wrap!
Or…is it? Aside from adding a custom legend which was part of the request, there’s also a number of “quality of life” features we could add, as well as some cleanup:
- Add a “width” field for the chart (so it’s not just full screen)
- Enable the user to toggle between Pie Chart and Doughnut Chart
- DRY out our JavaScript/React code to make it more modular and component driven
To run through every improvement that could be made step by step would result in a very long blog post, so instead I will highlight the overall direction, since you can review the Github repo to see the full extent of the refactoring.
GitHub: https://github.com/cheestudio/wp-simple-pie-chart-block
If you’re interested in the more advanced phase of the plugin development, join us on the journey to the next level! The rest of this tutorial assumes you have a decent amount of experience with Blocks, JS, and React.
Refactoring
Before we begin expanding our codebase and adding features, let’s take a step back and refactor the architecture of our block. For one, it doesn’t adhere to DRY coding standards, nor does it leverage modern JavaScript and React design patterns. Let’s begin with some basic high level organization on how we can better architect this plugin.
Folder and File Structure
Beforehand, we had everything stored in our edit.js
function file. This would quickly become quite disorganized, especially if we keep expanding features. Here’s what I would consider a well organized block that adheres (mostly) to the Single Responsibility Principle (SRP) design pattern:
├── src
│ ├── components
│ │ ├── ChartSettings.js
│ │ ├── PieChartControls.js
│ │ ├── PieChartDisplay.js
│ │ ├── SliceControls.js
│ │ ├── SliceEntries.js
│ │ ├── SliceEntry.js
│ │ ├── SliceLegend.js
│ ├── lib
│ │ ├── chartUtils.js
├── block.json
├── edit.js
├── editor.scss
├── index.js
├── render.php
├── style.scss
├── view.js
├── package.json
├── readme.txt
├── wp-simple-pie-chart-block.php
Slice Functions
The biggest issue with our existing block is that we had a lot of repetition in our Slice Attribute update functions. Ideally, we could implement a Reducer function here and consolidate all of those individual attribute/state updates into something much more dynamic and, DRY. First we’ll modify how the component values are passed up:
// SliceEntry.js examples
...
<TextControl
label={__('Slice Title', 'chee-blocks')}
value={slice.sliceTitle}
placeholder={__('e.g. Category Title', 'chee-blocks')}
onChange={(value) => onChange(id, 'sliceTitle', value)}
/>
<TextControl
label={__('Slice Value', 'chee-blocks')}
value={slice.sliceValue}
placeholder={__('e.g. $1,000', 'chee-blocks')}
onChange={(value) => onChange(id, 'sliceValue', value)}
/>
<RangeControl
label={__('Slice Percentage', 'chee-blocks')}
value={slice.slicePercentage}
onChange={(percentage) => onChange(id, 'slicePercentage', percentage)}
min={0}
max={100}
step={0.1}
/>
Next we’ll add in handleUpdateSlice()
, which will process all values and feed them into a common function:
// SliceEntries.js
import SliceEntry from './SliceEntry';
const SliceEntries = ({ slices, colorPalette, handleUpdateSlice, handleRemoveSlice }) => {
return (
<>
{slices.map((slice, index) => (
<SliceEntry
key={index}
id={index}
slice={slice}
colorPalette={colorPalette}
onChange={(id, attribute, value) => handleUpdateSlice(id, attribute, value)}
onRemoveSlice={handleRemoveSlice}
/>
))}
</>
);
};
export default SliceEntries;
We’ll link our handleUpdateSlice()
to instead leverage useReducer
:
// edit.js
// handleUpdateSlice() with Reducer
const [sliceState, dispatch] = useReducer(sliceReducer, slices);
const handleUpdateSlice = (id, attribute, value) => {
dispatch({ type: 'UPDATE_SLICE', id, attribute, value });
};
And finally, we’ll create our Reducer function that will consolidate all our various state and attributes for our Slices:
// chartUtils.js Slice Reducer
export const sliceReducer = (state, action) => {
switch (action.type) {
case 'UPDATE_SLICE':
return state.map((slice, index) =>
index === action.id ? { ...slice, [action.attribute]: action.value } : slice
);
case 'REMOVE_SLICE':
return state.filter((_, index) => index !== action.id);
case 'ADD_SLICE':
const newSliceColor = state.length % 2 === 0 ? '#666' : '#333';
return [...state,{ sliceTitle: '', sliceColor: newSliceColor, slicePercentage: 10, sliceValue: '' }];
default:
return state;
}
};
Repetition between Backend and Frontend
Did you notice in the first iteration that we were repeating blocks of code that were shared between the Block Editor and the frontend view? We can abstract all of that into a common file that both will draw from. We’ll move them into chartUtils.js
and export:
/* Slice Attrs */
export const sliceData = (slices) => ({
labels: slices.map(slice => slice.sliceTitle),
datasets: [{
data: slices.map(slice => slice.slicePercentage),
backgroundColor: slices.map(slice => slice.sliceColor),
value: slices.map(slice => slice.sliceValue),
hoverOffset: 4
}]
});
/* Chart Options */
export const chartOptions = {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: { display: false }
}
};
And we can now import them into our React components:
// edit.js
...
//common data
import { chartOptions, sliceData, sliceReducer } from './lib/chartUtils';
...
//common data
const data = sliceData(sliceState);
setAttributes({ slices: sliceState });
...
And our view.js
:
import { Chart as ChartJS, PieController, ArcElement, Tooltip, Legend, PieController } from 'chart.js';
import ChartDataLabels from 'chartjs-plugin-datalabels';
import { sliceData, chartOptions } from './lib/chartUtils'; //common data
ChartJS.register(ArcElement, Tooltip, Legend, PieController);
// Chart Options
const initChart = (chart) => {
const canvas = document.createElement('canvas');
const slices = JSON.parse(chart.dataset.slices);
const data = sliceData(slices); //common data
const chartInstance = new ChartJS(canvas, {
type: 'pie',
data: data, //common data
options: chartOptions, //common data
plugins: [ChartDataLabels],
});
Additional Features: Chart Style, Width and Custom Legend
Let’s add our other custom attributes to the pie chart block. First we expand our attributes
object:
"attributes": {
"slices": {
"type": "array",
"default": [
{
"sliceTitle": "",
"sliceColor": "#666",
"slicePercentage": 10,
"sliceValue": ""
}
]
},
"showLegend": {
"type": "boolean",
"default": true
},
"legendBG": {
"type": "string",
"default": "#fff"
},
"legendStyle": {
"type": "string",
"default": "line"
},
"chartType": {
"type": "string",
"default": "pie"
},
"chartWidth": {
"type": "number",
"default": 800
}
}
Custom Width and Legend Toggle
Let’s create a ChartSettings component that will allow us to toggle the chart type (Pie or Doughnut), set a width, and also toggle a custom legend:
import { __ } from '@wordpress/i18n';
import { ToggleControl, RadioControl, BaseControl, PanelBody, ColorPalette, RangeControl } from '@wordpress/components';
const ChartSettings = ({ attributes, setAttributes, colorPalette }) => {
const { showLegend, legendStyle, legendBG, chartType, chartWidth } = attributes;
const handleAttributeChange = (attribute) => (value) => setAttributes({ [attribute]: value });
return (
<PanelBody title={__('Pie Chart Settings', 'chee-blocks')}>
<RadioControl
label={__('Chart Type', 'chee-blocks')}
selected={chartType}
options={[
{ label: __('Pie', 'chee-blocks'), value: 'Pie' },
{ label: __('Doughnut', 'chee-blocks'), value: 'Doughnut' }
]}
onChange={handleAttributeChange('chartType')}
/>
<BaseControl
label={__('Chart Width', 'chee-blocks')}
id="chart-width-control"
>
<RangeControl
value={chartWidth}
onChange={handleAttributeChange('chartWidth')}
min={400}
max={1200}
step={10}
aria-label={__('Chart Width', 'chee-blocks')}
/>
</BaseControl>
<ToggleControl
label={__('Show Legend', 'chee-blocks')}
checked={showLegend}
onChange={handleAttributeChange('showLegend')}
/>
{showLegend && (
<>
<RadioControl
label={__('Legend Style', 'chee-blocks')}
selected={legendStyle}
options={[
{ label: __('Lines', 'chee-blocks'), value: 'line' },
{ label: __('Dots', 'chee-blocks'), value: 'dot' }
]}
onChange={handleAttributeChange('legendStyle')}
/>
<BaseControl
__nextHasNoMarginBottom
id="legend-bg-color"
label={__('Legend Background Color', 'chee-blocks')}
>
<ColorPalette
id="legend-bg-color"
aria-label={__('Color palette for legend background', 'chee-blocks')}
colors={colorPalette}
value={legendBG}
onChange={handleAttributeChange('legendBG')}
/>
</BaseControl>
</>
)}
</PanelBody>
);
};
export default ChartSettings;
Chart Type Component
Pie and Doughnut Charts are similar in nature, so allowing the user to choose one or the other makes a lot of sense for this simple plugin. We’ll create a Polymorphic Component to swap the chartType
, and we’ll also set up a useEffect
to handle the chartWidth
resize method that ships with chartJS.
import { forwardRef, useEffect, useRef } from '@wordpress/element';
import { Pie, Doughnut } from 'react-chartjs-2';
import { Chart as ChartJS, ArcElement, Tooltip, Legend } from 'chart.js';
import ChartDataLabels from 'chartjs-plugin-datalabels';
ChartJS.register(ArcElement, Tooltip, Legend);
const PieChartDisplay = forwardRef(({ chartOptions, data, chartType, chartWidth }, ref) => {
const ChartType = { Pie, Doughnut }[chartType] || Doughnut;
const chartRef = useRef(null);
useEffect(() => {
if (chartRef.current) {
chartRef.current.resize();
}
}, [chartWidth]);
return (
<div ref={ref} className="pie-chart-display">
<div className="pie-chart" style={{ maxWidth: chartWidth+'px', margin: "0 auto" }}>
<ChartType
ref={chartRef}
options={chartOptions}
data={data}
plugins={[ChartDataLabels]}
/>
</div>
</div>
);
});
export default PieChartDisplay;
Custom Legend
ChartJS has a basic legend built into it’s library, but we’re going to swap that out for something a bit more stylistic and functional. Implementing the styles are up to you, but check the GitHub repo plugin for a full example:
// render.php
<?php if (!empty($slices) && $attributes['showLegend']) : ?>
<ul class="wp-simple-pie-chart-block__legend" style="--legend-bg:<?= esc_attr($attributes['legendBG']); ?>;">
<?php foreach ($slices as $slice) : ?>
<li class="wp-simple-pie-chart-block__legend--entry <?= esc_attr($attributes['legendStyle']); ?>" style="--slice-color:<?= esc_attr($slice['sliceColor']); ?>;">
<div class="legend-info">
<div class="legend-title"><?= esc_html($slice['sliceTitle']); ?></div>
<div class="legend-value"><?= esc_html($slice['sliceValue']); ?></div>
</div>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
// view.js
// Show/hide slices on legend click events
const handleLegendClick = (event) => {
const legendItem = event.target.closest('.wp-simple-pie-chart-block__legend--entry');
if (!legendItem) return;
const chartContainer = legendItem.closest('.wp-simple-pie-chart-block__display').querySelector('.wp-simple-pie-chart-block-instance');
const chartInstance = chartContainer.chartInstance;
const sliceIndex = Array.from(legendItem.parentNode.children).indexOf(legendItem);
chartInstance.toggleDataVisibility(sliceIndex);
chartInstance.update();
legendItem.classList.toggle('active');
};
//Init Legends
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.wp-simple-pie-chart-block__legend--entry').forEach(legendItem => {
legendItem.addEventListener('click', handleLegendClick);
});
});
And here we have it: a more robust and stylized custom legend, that’s also interactive like the simple one that ships with ChartJS!
- Big Slice1000
- Medium Slice500
- Tiny Slice200
And a Doughnut Chart variation:
- Small Slice1000
- Medium Slice500
- Huge Slice4000
We’ve accomplished our goal in creating this simple but highly effective block. While it’s not chock full of bells and whistles, because we organized our code using modern React design patterns, we set ourselves up for success if we choose to expand this plugins’ features in the future. For future releases, I would love to see additional chart supports and multiple datasets. We’re in a position where we could add those features in with relative ease.
Soon this plugin will also be available in the WordPress Plugin directory, but in the meantime, you can download it from the repo! I hope it comes in handy. 🥧🍩