assets/js/llms-analytics.js
;/**
* LifterLMS Admin Reporting Widgets & Charts
*
* @since 3.0.0
* @since 3.17.2 Unknown.
* @since 3.33.1 Fix issue that produced series options not aligned with the chart data.
* @since 3.36.3 Added the `allow_clear` paramater when initializiing the `llmsStudentSelect2`.
* @since 4.3.3 Legends will automatically display on top of the chart.
* @since 4.5.1 Show sales reporting currency symbol based on LifterLMS site options.
* @version 7.3.0
*
*/( function( $, undefined ) {
window.llms = window.llms || {};
/**
* LifterLMS Admin Analytics.
*
* @since 3.0.0
* @since 3.5.0 Unknown
* @since 4.5.1 Added `opts` parameter.
* @since [verison] Early bail if no `#llms-analytics-json` is available.
*
* @param {Object} options Options object.
* @return {Object} Class instance.
*/
var Analytics = function( opts ) {
if ( ! $( '#llms-analytics-json' ).length ) {
return;
}
this.charts_loaded = false;
this.data = {};
this.query = $.parseJSON( $( '#llms-analytics-json' ).text() );
this.timeout = 8000;
this.options = opts;
this.$widgets = $( '.llms-widget[data-method]' );
/**
* Initializer
*
* @return void
* @since 3.0.0
* @version 3.0.0
*/
this.init = function() {
google.charts.load( 'current', {
packages: [
'corechart'
]
} );
google.charts.setOnLoadCallback( this.charts_ready );
this.bind();
this.load_widgets();
};
/**
* Bind DOM events.
*
* @since 3.0.0
* @since 3.36.3 Added the `allow_clear` paramater when initializiing the `llmsStudentSelect2`.
* @since 7.2.0 Added check for datepicker before initializing.
*
* @return {Void}
*/
this.bind = function() {
if ( $( '.llms-datepicker' ).length && $.fn.datepicker ) {
$( '.llms-datepicker' ).datepicker( {
dateFormat: 'yy-mm-dd',
maxDate: 0,
} );
}
$( '#llms-students-ids-filter' ).llmsStudentsSelect2( {
multiple: true,
placeholder: LLMS.l10n.translate( 'Filter by Student(s)' ),
allow_clear: true,
} );
$( 'a[href="#llms-toggle-filters"]' ).on( 'click', function( e ) {
e.preventDefault();
$( '.llms-analytics-filters' ).slideToggle( 100 );
} );
$( '#llms-custom-date-submit' ).on( 'click', function() {
$( 'input[name="range"]' ).val( 'custom' );
} );
$( '#llms-date-quick-filters a.llms-nav-link[data-range]' ).on( 'click', function( e ) {
e.preventDefault();
$( 'input[name="range"]' ).val( $( this ).attr( 'data-range' ) );
$( 'form.llms-reporting-nav' ).submit();
} );
};
/**
* Called by Google Charts when the library is loaded and ready
*
* @return void
* @since 3.0.0
* @version 3.0.0
*/
this.charts_ready = function() {
window.llms.analytics.charts_loaded = true;
window.llms.analytics.draw_chart();
};
/**
* Render the chart
*
* @since 3.0.0
* @since 3.17.6 Unknown
* @since 4.3.3 Force the legend to appear on top of the chart.
* @since 4.5.1 Display sales numbers according to the site's currency settings instead of the browser's locale.
*
* @return {void}
*/
this.draw_chart = function() {
if ( ! this.charts_loaded || ! this.is_loading_finished() ) {
return;
}
var el = document.getElementById( 'llms-charts-wrapper' );
if ( ! el ) {
return;
}
var self = this,
chart = new google.visualization.ComboChart( el ),
data = self.get_chart_data(),
options = {
legend: 'top',
chartArea: {
height: '75%',
width: '85%',
},
colors: ['#606C38','#E85D75','#EF8354','#C64191','#731963'],
height: 560,
lineWidth: 4,
seriesType: 'bars',
series: self.get_chart_series_options(),
vAxes: {
0: {
format: this.options.currency_format || 'currency',
},
1: {
format: '',
},
},
};
if ( data.length ) {
data = google.visualization.arrayToDataTable( data );
data.sort( [{column: 0}] );
chart.draw( data, options );
}
};
/**
* Check if a widget is still loading
*
* @return bool
* @since 3.0.0
* @version 3.0.0
*/
this.is_loading_finished = function() {
if ( $( '.llms-widget.is-loading' ).length ) {
return false;
}
return true;
};
/**
* Start loading all widgets on the current screen
*
* @return void
* @since 3.0.0
* @version 3.0.0
*/
this.load_widgets = function() {
var self = this;
this.$widgets.each( function() {
self.load_widget( $( this ) );
} );
};
/**
* Load a specific widget.
*
* @since 3.0.0
* @since 7.2.0 Change h1 tag to .llms-widget-content.
* @since 7.3.0 Append `_ajax_nonce` to the ajax data packet.
*
* @param {Object} $widget The jQuery selector of the widget element.
* @return {Void}
*/
this.load_widget = function( $widget ) {
var self = this,
method = $widget.attr( 'data-method' ),
$content = $widget.find( '.llms-widget-content' ),
$retry = $widget.find( '.llms-reload-widget' ),
content_text = LLMS.l10n.translate( 'Error' ),
status;
$widget.addClass( 'is-loading' );
$.ajax( {
data: {
action: 'llms_widget_' + method,
dates: self.query.dates,
courses: self.query.current_courses,
memberships: self.query.current_memberships,
students: self.query.current_students,
_ajax_nonce: window.llms.ajax_nonce,
},
method: 'POST',
timeout: self.timeout,
url: window.ajaxurl,
success: function( r ) {
status = 'success';
if ( 'undefined' !== typeof r.response ) {
content_text = r.response;
self.data[method] = {
chart_data: r.chart_data,
response: r.response,
results: r.results,
};
$retry.remove();
}
},
error: function( r ) {
status = 'error';
},
complete: function( r ) {
if ( 'error' === status ) {
if ( 'timeout' === r.statusText ) {
content_text = LLMS.l10n.translate( 'Request timed out' );
} else {
content_text = LLMS.l10n.translate( 'Error' );
}
if ( ! $retry.length ) {
$retry = $( '<a class="llms-reload-widget" href="#">' + LLMS.l10n.translate( 'Retry' ) + '</a>' );
$retry.on( 'click', function( e ) {
e.preventDefault();
self.load_widget( $widget );
} );
$widget.append( $retry );
}
}
$widget.removeClass( 'is-loading' );
$content.html( content_text );
self.widget_finished( $widget );
}
} );
};
/**
* Get the time in seconds between the queried dates
*
* @return int
* @since 3.0.0
* @version 3.0.0
*/
this.get_date_diff = function() {
var end = new Date( this.query.dates.end ),
start = new Date( this.query.dates.start );
return Math.abs( end.getTime() - start.getTime() );
};
/**
* Builds an object of data that can be used to, ultimately, draw the screen's chart
*
* @return obj
* @since 3.0.0
* @version 3.1.6
*/
this.get_chart_data_object = function() {
var self = this,
max_for_days = ( ( 1000 * 3600 * 24 ) * 30 ) * 4, // 4 months in seconds
diff = this.get_date_diff(),
data = {},
res, i, d, date;
for ( var method in self.data ) {
if ( ! self.data.hasOwnProperty( method ) ) {
continue;
}
if ( 'object' !== typeof self.data[ method ].chart_data || 'object' !== typeof self.data[ method ].results ) {
continue;
}
res = self.data[ method ].results;
if ( res ) {
for ( i = 0; i < res.length; i++ ) {
d = this.init_date( res[i].date );
// group by days
if ( diff <= max_for_days ) {
date = new Date( d.getFullYear(), d.getMonth(), d.getDate() );
}
// group by months
else {
date = new Date( d.getFullYear(), d.getMonth(), 1 );
}
if ( ! data[ date ] ) {
data[ date ] = this.get_empty_data_object( date )
}
switch ( self.data[ method ].chart_data.type ) {
case 'amount':
data[ date ][ method ] = data[ date ][ method ] + ( res[i][ self.data[ method ].chart_data.key ] * 1 );
break;
case 'count':
default:
data[ date ][ method ]++;
break;
}
}
}
}
return data;
};
/**
* Get the data google charts needs to initiate the current chart
*
* @return obj
* @since 3.0.0
* @version 3.0.0
*/
this.get_chart_data = function() {
var self = this,
obj = self.get_chart_data_object(),
data = self.get_chart_headers();
for ( var date in obj ) {
if ( ! obj.hasOwnProperty( date ) ) {
continue;
}
var row = [ obj[ date ]._date ];
for ( var item in obj[ date ] ) {
if ( ! obj[ date ].hasOwnProperty( item ) ) {
continue;
}
// skip meta items
if ( 0 === item.indexOf( '_' ) ) {
continue;
}
row.push( obj[ date ][ item ] );
}
data.push( row );
}
return data;
};
/**
* Get a stub of the data object used by this.get_data_object
*
* @param string date date to instantiate the object with
* @return obj
* @since 3.0.0
* @version 3.0.0
*/
this.get_empty_data_object = function( date ) {
var self = this,
obj = {
_date: date,
};
for ( var method in self.data ) {
if ( ! self.data.hasOwnProperty( method ) ) {
continue;
}
if ( self.data[ method ].chart_data ) {
obj[ method ] = 0;
}
}
return obj;
};
/**
* Builds an array of chart header data
*
* @return array
* @since 3.0.0
* @version 3.0.0
*/
this.get_chart_headers = function() {
var self = this,
h = [];
// date headers go first
h.push( {
label: LLMS.l10n.translate( 'Date' ),
id: 'date',
type: 'date',
} );
for ( var method in self.data ) {
if ( ! self.data.hasOwnProperty( method ) ) {
continue;
}
if ( self.data[ method ].chart_data ) {
h.push( self.data[ method ].chart_data.header );
}
}
return [ h ];
};
/**
* Get a object of series options needed to draw the chart.
*
* @since 3.0.0
* @since Fix issue that produced series options not aligned with the chart data.
*
* @return void
*/
this.get_chart_series_options = function() {
var self = this,
options = {}
i = 0;
for ( var method in self.data ) {
if ( ! self.data.hasOwnProperty( method ) ) {
continue;
}
if ( self.data[ method ].chart_data ) {
var type = self.data[ method ].chart_data.type;
options[ i ] = {
type: ( 'count' === type ) ? 'bars' : 'line',
targetAxisIndex: ( 'count' === type ) ? 1 : 0,
};
i++;
}
}
return options;
};
/**
* Instantiate a Date instance via a date string
*
* @param string string date string, expected format should be from php date( 'Y-m-d H:i:s' )
* @return obj
* @since 3.1.4
* @version 3.1.5
*/
this.init_date = function( string ) {
var parts, date, time;
parts = string.split( ' ' );
date = parts[0].split( '-' );
time = parts[1].split( ':' );
return new Date( date[0], date[1] - 1, date[2], time[0], time[1], time[2] );
};
/**
* Called when a widget is finished loading
* Updates the current chart with the new data from the widget
*
* @param obj $widget jQuery selector of the widget element
* @return void
* @since 3.0.0
* @version 3.0.0
*/
this.widget_finished = function( $widget ) {
if ( this.is_loading_finished() ) {
this.draw_chart();
}
};
// go
this.init();
// return
return this;
};
window.llms.analytics = new Analytics( window.llms.analytics || {} );
} )( jQuery );