Creating Custom Reports

Get Started

Creating Custom Reports

LearnDash Dashboard provides a unified reports architecture since version 7.5.0. You can create custom table reports and Chart.js charts that integrate with the REST API, DataTables, and access control system.


Report Types

TypeBase ClassOutput
TableLD<em>Dashboard</em>Report_BaseDataTable with CSV/Excel export
ChartLD<em>Dashboard</em>Chart_BaseChart.js visualization

LD<em>Dashboard</em>Chart<em>Base extends LD</em>Dashboard<em>Report</em>Base. Charts inherit all caching, permission, and REST API infrastructure.


LDDashboardReport_Base

Located at includes/reports/class-ld-dashboard-report-base.php.

Abstract Methods (required)

MethodReturnDescription
get_id(): stringstringUnique identifier (e.g., my-revenue-report). Used for REST endpoint and cache keys.
get_title(): stringstringHuman-readable translated title.
get_columns(): arrayarrayDataTables column definitions.
fetch_data( array $args ): arrayarrayRaw data fetch. Do not apply role filtering here.

Override Methods (optional)

MethodDefaultDescription
get_type(): string'table'Report type identifier.
filter<em>data</em>by<em>role( array $data, int $user</em>id ): arrayReturns data unchangedApply role-based row filtering.
user<em>can</em>view( int $user_id = 0 ): boolChecks LD<em>Dashboard</em>REST_PermissionsCustom permission logic.
get<em>cache</em>duration(): intHOUR<em>IN</em>SECONDSTransient cache TTL in seconds.
get<em>export</em>formats(): array['csv', 'excel']Supported export formats.
get<em>allowed</em>roles(): array['administrator', 'ld<em>instructor', 'group</em>leader']Roles that can access this report.
get<em>rest</em>args(): arrayStandard filter paramsREST API parameter schema.
get_meta(): arrayReport ID and typeAdditional metadata in the response.

Built-in Infrastructure

The base class handles automatically:

  • Cachingget<em>cached</em>data() stores results in WordPress transients. Cache key includes report ID, user ID, and a hash of the query args.
  • Role scopingget<em>accessible</em>course<em>ids() and get</em>accessible<em>user</em>ids() return ID arrays restricted by role (admin gets empty = no restriction).
  • REST callbackrest<em>callback( WP</em>REST<em>Request $request ) enforces permissions, extracts args, and returns a WP</em>REST_Response.
  • Student scoping — Students are automatically scoped to user<em>id = current</em>user_id in the REST callback.

LDDashboardChart_Base

Located at includes/reports/class-ld-dashboard-chart-base.php.

Extends LD<em>Dashboard</em>Report<em>Base. Override get</em>type() returns 'chart' automatically.

Additional Abstract Methods

MethodReturnDescription
get<em>chart</em>type(): stringstringChart.js type: 'bar', 'pie', 'doughnut', or 'line'.

Additional Override Methods

MethodDefaultDescription
format<em>chart</em>data( array $data ): arrayExpects labels and values keysTransform raw data to Chart.js dataset format.
get<em>chart</em>options(): arrayResponsive, legend topChart.js configuration options.
get<em>filter</em>options(): arrayyear, month, weekTime period filter options for the frontend.
get<em>export</em>formats(): array[]Charts do not export by default.

Registering Reports

Register your report class using LD<em>Dashboard</em>Report<em>Registry::register() inside the ld</em>dashboard<em>register</em>reports or ld<em>dashboard</em>register_charts action hook.

add_action( 'ld_dashboard_register_reports', function( $registry ) {
    LD_Dashboard_Report_Registry::register( 'course-revenue', 'My_Course_Revenue_Report' );
} );

add_action( 'ld_dashboard_register_charts', function( $registry ) {
    LD_Dashboard_Report_Registry::register( 'revenue-trend', 'My_Revenue_Trend_Chart' );
} );

register() instantiates your class and validates it extends the correct base. If the class does not exist or extends an incorrect base, it calls <em>doing</em>it_wrong().


REST API Auto-Registration

When you register a report, the REST endpoint is automatically created:

GET  /wp-json/ld-dashboard/v2/reports/course-revenue
POST /wp-json/ld-dashboard/v2/reports/course-revenue
DELETE /wp-json/ld-dashboard/v2/reports/course-revenue/cache

Your report’s get<em>rest</em>args() defines the accepted parameters. The cache clearing endpoint requires administrator access.


JavaScript Auto-Initialization

Render a report container in PHP templates using the registry helpers:

// Table report
echo LD_Dashboard_Report_Registry::render_table( 'course-revenue', array( 'course_id' => 123 ) );

// Chart
echo LD_Dashboard_Report_Registry::render_chart( 'revenue-trend', array( 'filter' => 'month' ) );

// Global function aliases
echo ld_dashboard_render_table( 'course-revenue' );
echo ld_dashboard_render_chart( 'revenue-trend' );

The rendered HTML uses data-ld-report or data-ld-chart attributes for JavaScript auto-initialization:

<!-- Table -->
<div class="ld-dashboard-report-table-wrapper">
    <table id="ld-report-table-course-revenue"
           class="ld-dashboard-table display ld-dashboard-datatable"
           data-ld-report="course-revenue"
           data-options='{"course_id":123}'
           cellspacing="0" width="100%">
    </table>
</div>

<!-- Chart -->
<div class="ld-dashboard-chart-wrapper">
    <div id="ld-report-chart-revenue-trend"
         class="ld-dashboard-chart-js"
         data-ld-chart="revenue-trend"
         data-options='{}'>
    </div>
</div>

The LDReportManager JavaScript module detects these attributes and initializes DataTables or Chart.js automatically.


Complete Example: Course Revenue Report

<?php
/**
 * Custom Course Revenue Report.
 *
 * @since 1.0.0
 */
class My_Course_Revenue_Report extends LD_Dashboard_Report_Base {

    /**
     * Get report ID.
     *
     * @since 1.0.0
     * @return string
     */
    public function get_id(): string {
        return 'course-revenue';
    }

    /**
     * Get report title.
     *
     * @since 1.0.0
     * @return string
     */
    public function get_title(): string {
        return __( 'Course Revenue', 'my-plugin' );
    }

    /**
     * Get column definitions.
     *
     * @since 1.0.0
     * @return array
     */
    protected function get_columns(): array {
        return array(
            array(
                'data'      => 'course_name',
                'title'     => __( 'Course', 'my-plugin' ),
                'visible'   => true,
                'orderable' => true,
            ),
            array(
                'data'      => 'sales',
                'title'     => __( 'Sales', 'my-plugin' ),
                'visible'   => true,
                'orderable' => true,
            ),
            array(
                'data'      => 'revenue',
                'title'     => __( 'Revenue', 'my-plugin' ),
                'visible'   => true,
                'orderable' => true,
            ),
        );
    }

    /**
     * Fetch revenue data.
     *
     * @since 1.0.0
     * @param array $args Query arguments.
     * @return array
     */
    protected function fetch_data( array $args ): array {
        global $wpdb;

        $course_id = isset( $args['course_id'] ) ? absint( $args['course_id'] ) : 0;

        // Example: join commission logs to course data.
        $query = $wpdb->prepare(
            "SELECT
                p.post_title AS course_name,
                COUNT(*) AS sales,
                SUM(l.course_price) AS revenue
            FROM {$wpdb->prefix}ld_dashboard_instructor_commission_logs l
            INNER JOIN {$wpdb->posts} p ON p.ID = l.course_id
            WHERE p.post_status = 'publish'
            " . ( $course_id ? $wpdb->prepare( 'AND l.course_id = %d', $course_id ) : '' ) . "
            GROUP BY l.course_id
            ORDER BY revenue DESC",
            // Note: prepare() args are only needed if course_id filter is active.
            ...( $course_id ? array( $course_id ) : array() )
        );

        return $wpdb->get_results( $query, ARRAY_A ) ?: array();
    }

    /**
     * Filter data by role — instructors see only their own courses.
     *
     * @since 1.0.0
     * @param array $data    Raw data.
     * @param int   $user_id User ID.
     * @return array
     */
    protected function filter_data_by_role( array $data, int $user_id ): array {
        // Admins see all.
        if ( learndash_is_admin_user( $user_id ) ) {
            return $data;
        }

        $accessible_ids = $this->get_accessible_course_ids( $user_id );

        if ( empty( $accessible_ids ) ) {
            return array();
        }

        return array_filter(
            $data,
            fn( $row ) => in_array( $row['course_id'], $accessible_ids, true )
        );
    }

    /**
     * Override cache duration to 30 minutes.
     *
     * @since 1.0.0
     * @return int
     */
    protected function get_cache_duration(): int {
        return 30 * MINUTE_IN_SECONDS;
    }
}

// Register the report.
add_action( 'ld_dashboard_register_reports', function() {
    LD_Dashboard_Report_Registry::register( 'course-revenue', 'My_Course_Revenue_Report' );
} );

Then render in a template:

// Render in a tab template or custom page.
echo ld_dashboard_render_table( 'course-revenue', array( 'course_id' => 0 ) );

Access the data via REST:

GET /wp-json/ld-dashboard/v2/reports/course-revenue?course_id=123

Filtering Report Data

Use the ld<em>dashboard</em>report_data filter to modify any report’s data after role filtering:

add_filter( 'ld_dashboard_report_data', function( $data, $report_id, $args ) {
    if ( 'course-revenue' === $report_id ) {
        // Format revenue as currency.
        foreach ( $data as &$row ) {
            $row['revenue'] = '$' . number_format( (float) $row['revenue'], 2 );
        }
    }
    return $data;
}, 10, 3 );

Use the report-specific filter for targeted changes:

add_filter( 'ld_dashboard_report_course-revenue_data', function( $response, $args ) {
    // Modify the full response array (columns, data, meta, etc.)
    return $response;
}, 10, 2 );
Last updated: March 4, 2026