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:

Ready and rearing to go…

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 Slice
    1000
  • Medium Slice
    500
  • Tiny Slice
    200

And a Doughnut Chart variation:

  • Small Slice
    1000
  • Medium Slice
    500
  • Huge Slice
    4000

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. 🥧🍩