Skip to content

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.


Type Base Class Output
Table LD_Dashboard_Report_Base DataTable with CSV/Excel export
Chart LD_Dashboard_Chart_Base Chart.js visualization

LD_Dashboard_Chart_Base extends LD_Dashboard_Report_Base. Charts inherit all caching, permission, and REST API infrastructure.


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

Method Return Description
get_id(): string string Unique identifier (e.g., my-revenue-report). Used for REST endpoint and cache keys.
get_title(): string string Human-readable translated title.
get_columns(): array array DataTables column definitions.
fetch_data( array $args ): array array Raw data fetch. Do not apply role filtering here.
Method Default Description
get_type(): string 'table' Report type identifier.
filter_data_by_role( array $data, int $user_id ): array Returns data unchanged Apply role-based row filtering.
user_can_view( int $user_id = 0 ): bool Checks LD_Dashboard_REST_Permissions Custom permission logic.
get_cache_duration(): int HOUR_IN_SECONDS Transient cache TTL in seconds.
get_export_formats(): array ['csv', 'excel'] Supported export formats.
get_allowed_roles(): array ['administrator', 'ld_instructor', 'group_leader'] Roles that can access this report.
get_rest_args(): array Standard filter params REST API parameter schema.
get_meta(): array Report ID and type Additional metadata in the response.

The base class handles automatically:

  • Cachingget_cached_data() stores results in WordPress transients. Cache key includes report ID, user ID, and a hash of the query args.
  • Role scopingget_accessible_course_ids() and get_accessible_user_ids() return ID arrays restricted by role (admin gets empty = no restriction).
  • REST callbackrest_callback( WP_REST_Request $request ) enforces permissions, extracts args, and returns a WP_REST_Response.
  • Student scoping — Students are automatically scoped to user_id = current_user_id in the REST callback.

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

Extends LD_Dashboard_Report_Base. Override get_type() returns 'chart' automatically.

Method Return Description
get_chart_type(): string string Chart.js type: 'bar', 'pie', 'doughnut', or 'line'.
Method Default Description
format_chart_data( array $data ): array Expects labels and values keys Transform raw data to Chart.js dataset format.
get_chart_options(): array Responsive, legend top Chart.js configuration options.
get_filter_options(): array year, month, week Time period filter options for the frontend.
get_export_formats(): array [] Charts do not export by default.

Register your report class using LD_Dashboard_Report_Registry::register() inside the ld_dashboard_register_reports or ld_dashboard_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 _doing_it_wrong().


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_rest_args() defines the accepted parameters. The cache clearing endpoint requires administrator access.


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.


<?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

Use the ld_dashboard_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 );