• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » Step-by-Step Guide to building a custom custom charts dashboard block for Gutenberg using REST API custom routes

Step-by-Step Guide to building a custom custom charts dashboard block for Gutenberg using REST API custom routes

Setting Up the WordPress Environment and Plugin Structure

Before diving into custom routes and Gutenberg blocks, we need a solid foundation. This involves creating a basic WordPress plugin structure and ensuring our development environment is ready. We’ll use a standard plugin directory and a main plugin file to register our custom routes and block.

Create a new directory within your WordPress installation’s wp-content/plugins/ folder. Let’s name it custom-charts-dashboard. Inside this directory, create a main PHP file, also named custom-charts-dashboard.php.

Registering Custom REST API Routes

WordPress’s REST API is extensible. We can register custom endpoints to serve data that isn’t natively available. For our charts dashboard, we’ll create an endpoint to fetch sample data. This involves hooking into the rest_api_init action.

In your custom-charts-dashboard.php file, add the following PHP code:

<?php
/**
 * Plugin Name: Custom Charts Dashboard
 * Description: A custom Gutenberg block for displaying charts using REST API data.
 * Version: 1.0
 * Author: Your Name
 */

// Prevent direct access to the file.
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * Register custom REST API routes.
 */
function ccd_register_custom_routes() {
    register_rest_route( 'custom-charts/v1', '/data', array(
        'methods'  => 'GET',
        'callback' => 'ccd_get_chart_data',
        'permission_callback' => '__return_true', // For simplicity, allow public access. Adjust for production.
    ) );
}
add_action( 'rest_api_init', 'ccd_register_custom_routes' );

/**
 * Callback function to return sample chart data.
 *
 * @param WP_REST_Request $request Full data about the request.
 * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
 */
function ccd_get_chart_data( WP_REST_Request $request ) {
    // In a real-world scenario, you would fetch this data from your database,
    // an external API, or perform complex calculations.
    $data = array(
        'labels' => array( 'January', 'February', 'March', 'April', 'May', 'June' ),
        'datasets' => array(
            array(
                'label' => 'Sales',
                'data'  => array( 65, 59, 80, 81, 56, 55 ),
                'backgroundColor' => 'rgba(75, 192, 192, 0.2)',
                'borderColor'     => 'rgba(75, 192, 192, 1)',
                'borderWidth'     => 1,
            ),
            array(
                'label' => 'Expenses',
                'data'  => array( 28, 48, 40, 19, 86, 27 ),
                'backgroundColor' => 'rgba(255, 99, 132, 0.2)',
                'borderColor'     => 'rgba(255, 99, 132, 1)',
                'borderWidth'     => 1,
            ),
        ),
    );

    return new WP_REST_Response( $data, 200 );
}
?>

This code defines a new route at /wp-json/custom-charts/v1/data. The ccd_get_chart_data function generates a simple JSON payload representing chart data. For production, you’d replace this with actual data retrieval logic and implement proper authentication/authorization in the permission_callback.

Building the Gutenberg Block: Registration and Editor Components

Now, let’s create the Gutenberg block. This involves registering the block type and defining its behavior in the editor. We’ll use JavaScript for this. First, create a build directory within your plugin folder for compiled assets, and inside that, a src directory for our source JavaScript and CSS files.

Create build/index.js and build/style.scss (or .css) in your plugin directory.

Block Registration (build/index.js)

This JavaScript file will register our block. We’ll use the @wordpress/blocks package. For a production setup, you’d typically use a build tool like Webpack or Rollup to compile your ESNext JavaScript and SCSS. For this example, we’ll assume a simplified setup where you’re directly enqueuing a compiled index.js and style.css.

Let’s create the core registration logic in build/index.js. This will define the block’s attributes, edit function (for the editor interface), and save function (for how it renders on the frontend).

// Import necessary WordPress packages.
const { registerBlockType } = wp.blocks;
const { Component } = wp.element;
const { InspectorControls } = wp.editor;
const { PanelBody, TextControl, SelectControl } = wp.components;
const { __ } = wp.i18n;

// Import Chart.js and a wrapper component.
// For a real project, you'd enqueue these separately or use a bundler.
// For this example, we'll assume Chart.js is available globally or imported.
// import Chart from 'chart.js/auto'; // If using a bundler

/**
 * Component to render the chart in the editor.
 */
class ChartEditor extends Component {
    constructor(props) {
        super(props);
        this.state = {
            chartData: null,
            chartType: 'bar', // Default chart type
            error: null,
        };
        this.chartInstance = null; // To hold the Chart.js instance
    }

    componentDidMount() {
        this.fetchChartData();
    }

    componentDidUpdate(prevProps) {
        // Re-fetch data if attributes change that affect data fetching (e.g., API endpoint)
        if (prevProps.attributes.apiEndpoint !== this.props.attributes.apiEndpoint) {
            this.fetchChartData();
        }
        // Re-render chart if data or type changes
        if (this.state.chartData !== null && this.chartInstance) {
            this.updateChart();
        }
    }

    componentWillUnmount() {
        if (this.chartInstance) {
            this.chartInstance.destroy();
        }
    }

    fetchChartData() {
        const { apiEndpoint } = this.props.attributes;
        const endpoint = apiEndpoint || '/wp-json/custom-charts/v1/data'; // Default endpoint

        fetch(endpoint)
            .then(response => {
                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }
                return response.json();
            })
            .then(data => {
                this.setState({ chartData: data, error: null });
            })
            .catch(error => {
                console.error('Error fetching chart data:', error);
                this.setState({ chartData: null, error: __('Error loading chart data. Please check the API endpoint and your network connection.', 'custom-charts-dashboard') });
            });
    }

    updateChart() {
        const { chartData, chartType } = this.state;
        const ctx = this.refs.canvas.getContext('2d');

        if (this.chartInstance) {
            this.chartInstance.destroy(); // Destroy previous instance
        }

        if (!chartData || !chartData.labels || !chartData.datasets) {
            return; // Don't render if data is incomplete
        }

        // Prepare datasets for Chart.js
        const datasets = chartData.datasets.map(dataset => ({
            label: dataset.label,
            data: dataset.data,
            backgroundColor: dataset.backgroundColor || 'rgba(0,0,0,0.1)',
            borderColor: dataset.borderColor || 'rgba(0,0,0,1)',
            borderWidth: dataset.borderWidth || 1,
        }));

        this.chartInstance = new Chart(ctx, {
            type: chartType,
            data: {
                labels: chartData.labels,
                datasets: datasets,
            },
            options: {
                responsive: true,
                maintainAspectRatio: false,
                scales: {
                    y: {
                        beginAtZero: true,
                    },
                },
            },
        });
    }

    render() {
        const { chartData, error } = this.state;
        const { setAttributes, attributes } = this.props;
        const { chartType } = attributes;

        // Update chart when chartType attribute changes
        if (this.state.chartType !== chartType) {
            this.setState({ chartType: chartType }, () => {
                this.updateChart();
            });
        }

        return (
            <div>
                <InspectorControls>
                    <PanelBody title={__('Chart Settings', 'custom-charts-dashboard')} initialOpen={true}>
                        <TextControl
                            label={__('API Endpoint', 'custom-charts-dashboard')}
                            value={attributes.apiEndpoint}
                            onChange={(value) => setAttributes({ apiEndpoint: value })}
                            help={__('Enter the URL for your data source. Defaults to /wp-json/custom-charts/v1/data', 'custom-charts-dashboard')}
                        />
                        <SelectControl
                            label={__('Chart Type', 'custom-charts-dashboard')}
                            value={chartType}
                            options={[
                                { label: __('Bar', 'custom-charts-dashboard'), value: 'bar' },
                                { label: __('Line', 'custom-charts-dashboard'), value: 'line' },
                                { label: __('Pie', 'custom-charts-dashboard'), value: 'pie' },
                                { label: __('Doughnut', 'custom-charts-dashboard'), value: 'doughnut' },
                            ]}
                            onChange={(value) => setAttributes({ chartType: value })}
                        />
                    </PanelBody>
                </InspectorControls>

                <div style={{ height: '300px', width: '100%' }}>
                    {error && <p style={{ color: 'red' }}>{error}</p>}
                    {!error && !chartData && <p>{__('Loading chart data...', 'custom-charts-dashboard')}</p>}
                    {!error && chartData && <canvas ref="canvas" />}
                </div>
            </div>
        );
    }
}

/**
 * Register the block.
 */
registerBlockType( 'custom-charts-dashboard/block', {
    title: __( 'Custom Charts Dashboard', 'custom-charts-dashboard' ),
    icon: 'chart-bar', // WordPress Dashicon
    category: 'widgets', // Or 'common', 'layout', etc.
    attributes: {
        apiEndpoint: {
            type: 'string',
            default: '/wp-json/custom-charts/v1/data',
        },
        chartType: {
            type: 'string',
            default: 'bar',
        },
    },

    edit: (props) => {
        // Ensure Chart.js is loaded before rendering the editor component.
        // In a real scenario, this would be handled by enqueuing scripts.
        if (typeof Chart === 'undefined') {
            return <p>{__('Chart.js is not loaded. Please ensure it is enqueued.', 'custom-charts-dashboard')}</p>;
        }
        return <ChartEditor {...props} />;
    },

    save: (props) => {
        // The save function should return static HTML that can be rendered on the frontend.
        // Since our chart is dynamic and relies on JavaScript, we'll return a placeholder
        // and enqueue a script to render the chart on the frontend.
        const { apiEndpoint, chartType } = props.attributes;
        return (
            <div
                className="custom-charts-dashboard-frontend"
                data-api-endpoint={apiEndpoint}
                data-chart-type={chartType}
                style={{ height: '300px', width: '100%' }}
            >
                <!-- Chart will be rendered here by JavaScript -->
            </div>
        );
    },
} );

Frontend Rendering Logic (build/index.js – continued)

The save function in Gutenberg blocks should return static HTML. For dynamic content like charts, this means returning a placeholder element that JavaScript can later populate. We’ll add a separate script to handle this frontend rendering.

Add the following function to your build/index.js file to handle frontend chart rendering:

/**
 * Renders charts on the frontend.
 */
function renderFrontendCharts() {
    const chartContainers = document.querySelectorAll('.custom-charts-dashboard-frontend');

    chartContainers.forEach(container => {
        const apiEndpoint = container.dataset.apiEndpoint;
        const chartType = container.dataset.chartType;

        if (typeof Chart === 'undefined') {
            console.error('Chart.js is not loaded. Cannot render frontend chart.');
            container.innerHTML = '<p style="color: red;">Chart.js is not loaded.</p>';
            return;
        }

        fetch(apiEndpoint)
            .then(response => {
                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }
                return response.json();
            })
            .then(data => {
                const canvas = document.createElement('canvas');
                container.innerHTML = ''; // Clear placeholder
                container.appendChild(canvas);
                const ctx = canvas.getContext('2d');

                if (!data || !data.labels || !data.datasets) {
                    container.innerHTML = '<p>No valid data to display.</p>';
                    return;
                }

                // Prepare datasets for Chart.js
                const datasets = data.datasets.map(dataset => ({
                    label: dataset.label,
                    data: dataset.data,
                    backgroundColor: dataset.backgroundColor || 'rgba(0,0,0,0.1)',
                    borderColor: dataset.borderColor || 'rgba(0,0,0,1)',
                    borderWidth: dataset.borderWidth || 1,
                }));

                new Chart(ctx, {
                    type: chartType,
                    data: {
                        labels: data.labels,
                        datasets: datasets,
                    },
                    options: {
                        responsive: true,
                        maintainAspectRatio: false,
                        scales: {
                            y: {
                                beginAtZero: true,
                            },
                        },
                    },
                });
            })
            .catch(error => {
                console.error('Error fetching chart data for frontend:', error);
                container.innerHTML = '<p style="color: red;">Error loading chart data.</p>';
            });
    });
}

// Execute on DOMContentLoaded for the frontend.
document.addEventListener('DOMContentLoaded', renderFrontendCharts);

// Also re-render if the block is loaded dynamically via AJAX (e.g., in Gutenberg editor previews or dynamic blocks).
// This is a simplified approach; a more robust solution might involve observing DOM changes.
// For Gutenberg, the editor's rendering mechanism usually handles this.
// If you encounter issues with dynamic blocks, consider using wp.hooks.addAction('blockEditor.BlockList.registerBlockType', ...)
// or similar mechanisms to re-initialize charts when blocks are added/updated.

Enqueuing Scripts and Styles

We need to enqueue our JavaScript (which includes Chart.js and our block logic) and CSS. This is done in the main plugin file using wp_enqueue_script and wp_enqueue_style.

Add the following to your custom-charts-dashboard.php file:

/**
 * Enqueue block editor assets.
 */
function ccd_enqueue_block_editor_assets() {
    // Enqueue Chart.js library.
    // For production, consider hosting this locally or using a CDN.
    wp_enqueue_script(
        'chartjs',
        'https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.min.js', // Use a specific version
        array(),
        '3.9.1',
        true // Load in footer
    );

    // Enqueue our block's JavaScript file.
    wp_enqueue_script(
        'custom-charts-dashboard-block-editor',
        plugin_dir_url( __FILE__ ) . 'build/index.js',
        array( 'wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n', 'chartjs' ), // Dependencies
        filemtime( plugin_dir_path( __FILE__ ) . 'build/index.js' ),
        true // Load in footer
    );

    // Enqueue block styles for the editor.
    wp_enqueue_style(
        'custom-charts-dashboard-block-editor-style',
        plugin_dir_url( __FILE__ ) . 'build/style.css', // Assuming you have a style.css
        array( 'wp-edit-blocks' ),
        filemtime( plugin_dir_path( __FILE__ ) . 'build/style.css' )
    );
}
add_action( 'enqueue_block_editor_assets', 'ccd_enqueue_block_editor_assets' );

/**
 * Enqueue block assets for the frontend.
 */
function ccd_enqueue_block_frontend_assets() {
    // Enqueue Chart.js library for the frontend.
    wp_enqueue_script(
        'chartjs-frontend',
        'https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.min.js',
        array(),
        '3.9.1',
        true
    );

    // Enqueue a separate script for frontend rendering if needed,
    // or ensure index.js handles both editor and frontend.
    // For simplicity, we'll assume index.js contains the frontend logic.
    // If index.js is only for editor, you'd create a separate frontend.js.
    // For this example, index.js contains both.
    // If you need a separate frontend script:
    /*
    wp_enqueue_script(
        'custom-charts-dashboard-frontend',
        plugin_dir_url( __FILE__ ) . 'build/frontend.js', // Assuming you create frontend.js
        array( 'chartjs-frontend' ),
        filemtime( plugin_dir_path( __FILE__ ) . 'build/frontend.js' ),
        true
    );
    */
}
add_action( 'wp_enqueue_scripts', 'ccd_enqueue_block_frontend_assets' );

Note on Build Process: The example assumes you have a build/index.js and build/style.css. In a real-world plugin, you would use a JavaScript build tool (like Webpack, Rollup, or Parcel) configured to compile your source files (e.g., from a src/ directory) into these production-ready files. You would also handle SCSS compilation to CSS.

Styling the Block

Create a build/style.css file for basic styling. This CSS will apply to both the editor and the frontend.

.wp-block-custom-charts-dashboard-block {
    border: 1px solid #ddd;
    padding: 15px;
    margin-bottom: 15px;
    background-color: #f9f9f9;
}

.wp-block-custom-charts-dashboard-block canvas {
    display: block; /* Ensure canvas takes up its space */
}

.custom-charts-dashboard-frontend {
    position: relative; /* For potential absolute positioning of loaders/errors */
}

Putting It All Together and Testing

1. **Activate the Plugin:** Place the custom-charts-dashboard folder in your wp-content/plugins/ directory and activate it via the WordPress admin dashboard.

2. **Build Assets:** If you’re using a build tool, run your build command (e.g., npm run build) to generate the build/index.js and build/style.css files.

3. **Add the Block:** Create or edit a post/page. In the Gutenberg editor, search for “Custom Charts Dashboard” and add the block.

4. **Configure and View:** In the block’s settings sidebar (Inspector Controls), you can change the chart type and, if necessary, the API endpoint. The chart should render in the editor. Save the post and view it on the frontend to see the chart rendered there.

Advanced Considerations and Next Steps

Error Handling and Permissions: The current permission_callback is set to __return_true for simplicity. In a production environment, you must implement robust checks to ensure only authorized users can access the data. This might involve checking user capabilities, nonces, or API keys.

Data Fetching: Replace the hardcoded data in ccd_get_chart_data with actual database queries or external API calls. Consider caching strategies for frequently accessed data to improve performance.

Dynamic Data Updates: For real-time dashboards, explore WebSockets or periodic polling mechanisms. For Gutenberg, consider using the useSelect hook to subscribe to data changes within the editor.

Chart Customization: Expose more Chart.js options (colors, tooltips, legends, etc.) as block attributes and controls in the Inspector Controls for greater user flexibility.

Internationalization (i18n): Ensure all user-facing strings are wrapped in WordPress’s translation functions (e.g., __(), _x()) and that a languages directory is set up for your plugin.

Build Process Robustness: Implement a comprehensive build process using Webpack or similar tools. This should handle JavaScript transpilation (ESNext to ES5), minification, SCSS compilation, and asset versioning (cache busting).

Accessibility: Ensure your charts are accessible. This might involve providing alternative text descriptions or data tables for users who cannot perceive visual information.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • Troubleshooting caching race conditions in production when using modern ACF Pro dynamic fields wrappers
  • Troubleshooting hook execution order overrides in production when using modern Classic Core PHP wrappers
  • Implementing automated compliance reporting for custom event ticket registers ledgers using TCPDF generator script
  • Step-by-Step Guide: Offloading high-frequency custom product catalogs metadata writes to a Redis KV store
  • Building secure B2B pricing grids with custom Metadata API (add_post_meta) endpoints and role overrides

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (629)
  • Desktop Applications (14)
  • DevOps (7)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (4)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (836)
  • PHP (5)
  • PHP Development (34)
  • Plugins & Themes (244)
  • Programming Languages (9)
  • Python (20)
  • Ruby on Rails (1)
  • Security & Compliance (608)
  • SEO & Growth (492)
  • Server (23)
  • Ubuntu (9)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (214)
  • WordPress Theme Development (357)

Recent Posts

  • Troubleshooting caching race conditions in production when using modern ACF Pro dynamic fields wrappers
  • Troubleshooting hook execution order overrides in production when using modern Classic Core PHP wrappers
  • Implementing automated compliance reporting for custom event ticket registers ledgers using TCPDF generator script

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (836)
  • Debugging & Troubleshooting (629)
  • Security & Compliance (608)
  • SEO & Growth (492)
  • Business & Monetization (390)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala