# learnpress/4.4.4/assets/src/js/admin/statistics/chart.js

LearnPress – WordPress LMS Plugin for Create and Sell Online Courses, version 4.4.4. 238 lines.

- Page: https://pluginprobe.com/plugins/learnpress/4.4.4/code/assets/src/js/admin/statistics/chart.js
- Raw: https://pluginprobe.com/plugins/learnpress/4.4.4/raw/assets/src/js/admin/statistics/chart.js
- Modified: 2026-08-03T03:26:42+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/learnpress/4.4.4/code/assets/src/js/admin/statistics/chart.js#L10-L20`.

```javascript
/**
 * Line chart renderer wrapping Chart.js — single or dual-axis.
 *
 * Pure renderer: no fetching, no state mutation. Reuses an existing chart
 * instance ( Chart.getChart ) instead of recreating, like the legacy
 * initStatisticChart did.
 *
 * @since 4.4.2
 * @version 1.0.0
 */

import Chart from 'chart.js/auto';
import { getStatsConfig, getStatsI18n } from './api.js';

const DEFAULT_COLORS = [ '#2271b1', '#00a32a' ];
const EMPTY_STATE_CLASS = 'lp-stats-chart-empty';

/**
 * Show/hide the empty state next to the canvas.
 *
 * @param {Element} canvas
 * @param {boolean} show
 */
const toggleEmptyState = ( canvas, show ) => {
	const wrapper = canvas.parentElement;
	if ( ! wrapper ) {
		return;
	}

	let elEmpty = wrapper.querySelector( `.${ EMPTY_STATE_CLASS }` );
	if ( show && ! elEmpty ) {
		elEmpty = document.createElement( 'p' );
		elEmpty.className = EMPTY_STATE_CLASS;
		elEmpty.textContent = getStatsI18n( 'noData', 'No data for this period.' );
		wrapper.appendChild( elEmpty );
	}

	if ( elEmpty ) {
		elEmpty.style.display = show ? '' : 'none';
	}
	canvas.style.display = show ? 'none' : 'block';
};

/**
 * One-off locale date formatting. Fine for a handful of dates ( labels, a
 * custom-range caption ); for per-label chart axes build a single formatter
 * and reuse it instead ( see granularityLabelFormatter ).
 *
 * @param {Date}   date
 * @param {Object} options Intl.DateTimeFormat options.
 * @return {string}
 */
export const intlFormat = ( date, options ) =>
	new Intl.DateTimeFormat( undefined, options ).format( date );

/**
 * Do all 'Y-m-d' day labels fall inside a single calendar month?
 * When they cross a month boundary the axis must show the month, otherwise
 * "30, 1, 2" is ambiguous.
 *
 * @param {Array} labels
 * @return {boolean}
 */
const daysWithinOneMonth = ( labels ) => {
	const months = labels.map( ( label ) => String( label ).slice( 0, 7 ) );
	return months.every( ( m ) => m === months[ 0 ] );
};

/**
 * Axis label formatter for a chart payload's `granularity` marker
 * ( PeriodRange->granularity, set server-side by PeriodResolver ):
 *
 * - hour   int 0–23   → "14h"
 * - day    'Y-m-d'    → "Tue 14" ( ≤ 7 points, single month ) / "Jul 14"
 * - month  'mm-YYYY'  → "Jul 26"-style short month + 2-digit year
 *
 * The returned closure captures a single Intl formatter ( built once here, not
 * per label ), so a 90-point chart formats against one instance. Unparsable
 * labels pass through untouched — never throws.
 *
 * @param {string} granularity Marker from the payload.
 * @param {Array}  labels      Full label set ( picks the day format density ).
 * @return {Function|null} ( label ) => string, or null for unknown markers.
 */
export const granularityLabelFormatter = ( granularity, labels = [] ) => {
	switch ( granularity ) {
		case 'hour':
			return ( label ) => `${ label }h`;

		case 'day': {
			// Weekday reads best for a short, single-month range; anything
			// crossing a month shows the month so labels like "Jun 30 / Jul 1"
			// stay unambiguous.
			const options =
				labels.length <= 7 && daysWithinOneMonth( labels )
					? { weekday: 'short', day: 'numeric' }
					: { month: 'short', day: 'numeric' };
			const fmt = new Intl.DateTimeFormat( undefined, options );
			return ( label ) => {
				const date = new Date( `${ label }T00:00:00` );
				return isNaN( date.getTime() ) ? String( label ) : fmt.format( date );
			};
		}

		case 'month': {
			// Labels are 'mm-YYYY'.
			const fmt = new Intl.DateTimeFormat( undefined, { month: 'short', year: '2-digit' } );
			return ( label ) => {
				const parts = String( label ).split( '-' );
				const month = parseInt( parts[ 0 ], 10 );
				if ( 2 === parts.length && month >= 1 && month <= 12 ) {
					return fmt.format( new Date( parseInt( parts[ 1 ], 10 ), month - 1, 1 ) );
				}
				return String( label );
			};
		}

		default:
			// Unknown markers render as-is; Chart.js stringifies them for the axis.
			return null;
	}
};

/**
 * Render (or update) a line chart.
 *
 * @param {string} canvasSelector e.g. '#net-sales-chart-content'.
 * @param {Object} chartData      { labels, datasets: [ { label, data, color, yAxisID } ], xLabel,
 *                                  granularity? — enables the shared axis label formatter }.
 * @param {Object} config         { yCurrency?: boolean (default true when 2 datasets),
 *                                  formatLabel?: ( label, index ) => string — overrides granularity }.
 * @return {Chart|null} Chart instance, or null when canvas missing / no data.
 */
export const renderLineChart = ( canvasSelector, chartData = {}, config = {} ) => {
	const canvas = document.querySelector( canvasSelector );
	if ( ! canvas ) {
		return null;
	}

	const { datasets = [], xLabel = '', granularity = '' } = chartData;
	let { labels = [] } = chartData;
	const hasData =
		datasets.length > 0 &&
		datasets.some( ( dataset ) => ( dataset.data || [] ).length > 0 );

	if ( ! hasData ) {
		const existing = Chart.getChart( canvas );
		if ( existing ) {
			existing.destroy();
		}
		toggleEmptyState( canvas, true );
		return null;
	}

	toggleEmptyState( canvas, false );

	const formatLabel =
		'function' === typeof config.formatLabel
			? config.formatLabel
			: granularityLabelFormatter( granularity, labels );
	if ( formatLabel ) {
		labels = labels.map( ( label, index ) => formatLabel( label, index ) );
	}

	const isDual = datasets.length > 1;
	const yCurrency = config.yCurrency ?? isDual;
	const currencySymbol = getStatsConfig().currencySymbol || '';

	const chartDatasets = datasets.map( ( dataset, index ) => {
		const color = dataset.color || DEFAULT_COLORS[ index % DEFAULT_COLORS.length ];
		return {
			label: dataset.label || '',
			data: dataset.data || [],
			borderColor: color,
			backgroundColor: color,
			borderWidth: 2,
			yAxisID: dataset.yAxisID || ( isDual && index > 0 ? 'y1' : 'y' ),
		};
	} );

	const scales = {
		y: {
			min: 0,
			position: 'left',
			ticks: yCurrency
				? { callback: ( value ) => currencySymbol + value }
				: {},
		},
		x: {
			title: {
				display: !! xLabel,
				text: xLabel,
				align: 'end',
			},
		},
	};

	if ( isDual ) {
		scales.y1 = {
			min: 0,
			position: 'right',
			grid: { drawOnChartArea: false },
			ticks: { precision: 0 },
		};
	}

	const existing = Chart.getChart( canvas );
	if ( existing ) {
		// Axis set changed (1 ↔ 2 lines) is easier rebuilt than migrated.
		if ( existing.data.datasets.length !== chartDatasets.length ) {
			existing.destroy();
		} else {
			existing.data.labels = labels;
			chartDatasets.forEach( ( dataset, index ) => {
				existing.data.datasets[ index ].data = dataset.data;
				existing.data.datasets[ index ].label = dataset.label;
			} );
			existing.config.options.scales.x.title.text = xLabel;
			existing.update();
			return existing;
		}
	}

	return new Chart( canvas, {
		type: 'line',
		data: { labels, datasets: chartDatasets },
		options: {
			responsive: true,
			maintainAspectRatio: false,
			aspectRatio: 0.8,
			plugins: {
				legend: { display: isDual },
			},
			scales,
		},
	} );
};

```
