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
| Type | Base Class | Output |
|---|---|---|
| Table | LD<em>Dashboard</em>Report_Base | DataTable with CSV/Excel export |
| Chart | LD<em>Dashboard</em>Chart_Base | Chart.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)
| 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. |
Override Methods (optional)
| Method | Default | Description |
|---|---|---|
get_type(): string | 'table' | Report type identifier. |
filter<em>data</em>by<em>role( array $data, int $user</em>id ): array | Returns data unchanged | Apply role-based row filtering. |
user<em>can</em>view( int $user_id = 0 ): bool | Checks LD<em>Dashboard</em>REST_Permissions | Custom permission logic. |
get<em>cache</em>duration(): int | HOUR<em>IN</em>SECONDS | Transient 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(): array | Standard filter params | REST API parameter schema. |
get_meta(): array | Report ID and type | Additional metadata in the response. |
Built-in Infrastructure
The base class handles automatically:
- Caching —
get<em>cached</em>data()stores results in WordPress transients. Cache key includes report ID, user ID, and a hash of the query args. - Role scoping —
get<em>accessible</em>course<em>ids()andget</em>accessible<em>user</em>ids()return ID arrays restricted by role (admin gets empty = no restriction). - REST callback —
rest<em>callback( WP</em>REST<em>Request $request )enforces permissions, extracts args, and returns aWP</em>REST_Response. - Student scoping — Students are automatically scoped to
user<em>id = current</em>user_idin 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
| Method | Return | Description |
|---|---|---|
get<em>chart</em>type(): string | string | Chart.js type: 'bar', 'pie', 'doughnut', or 'line'. |
Additional Override Methods
| Method | Default | Description |
|---|---|---|
format<em>chart</em>data( array $data ): array | Expects labels and values keys | Transform raw data to Chart.js dataset format. |
get<em>chart</em>options(): array | Responsive, legend top | Chart.js configuration options. |
get<em>filter</em>options(): array | year, month, week | Time 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 );
