0.6 ? '#1f2430' : '#ffffff';
}
/** Hue (degrees), saturation and lightness (0 to 1) of a colour */
function eacfa_to_hsl($hex) {
list($r, $g, $b) = eacfa_hex_to_rgb($hex);
$r /= 255;
$g /= 255;
$b /= 255;
$max = max($r, $g, $b);
$min = min($r, $g, $b);
$l = ($max + $min) / 2;
if ($max === $min) {
return array(0.0, 0.0, $l);
}
$d = $max - $min;
$s = $l > 0.5 ? $d / (2 - $max - $min) : $d / ($max + $min);
if ($max === $r) {
$h = ($g - $b) / $d + ($g < $b ? 6 : 0);
} elseif ($max === $g) {
$h = ($b - $r) / $d + 2;
} else {
$h = ($r - $g) / $d + 4;
}
return array($h * 60, $s, $l);
}
function eacfa_hue_to_channel($p, $q, $t) {
if ($t < 0) {
$t += 1;
}
if ($t > 1) {
$t -= 1;
}
if ($t < 1 / 6) {
return $p + ($q - $p) * 6 * $t;
}
if ($t < 1 / 2) {
return $q;
}
if ($t < 2 / 3) {
return $p + ($q - $p) * (2 / 3 - $t) * 6;
}
return $p;
}
/** Hex colour from hue (degrees), saturation and lightness */
function eacfa_from_hsl($hue, $sat, $light) {
$h = fmod(fmod($hue, 360) + 360, 360) / 360;
$s = max(0, min(1, $sat));
$l = max(0, min(1, $light));
if ($s == 0) {
$r = $l;
$g = $l;
$b = $l;
} else {
$q = $l < 0.5 ? $l * (1 + $s) : $l + $s - $l * $s;
$p = 2 * $l - $q;
$r = eacfa_hue_to_channel($p, $q, $h + 1 / 3);
$g = eacfa_hue_to_channel($p, $q, $h);
$b = eacfa_hue_to_channel($p, $q, $h - 1 / 3);
}
return eacfa_rgb_to_hex(array($r * 255, $g * 255, $b * 255));
}
/**
* Every colour the plugin draws with, derived from the primary colour. Cached for the request.
*/
function eacfa_palette() {
static $palette = null;
if ($palette !== null) {
return $palette;
}
$primary = eacfa_primary_color();
list($hue, $sat, $light) = eacfa_to_hsl($primary);
$dark_accent = eacfa_mix($primary, '#ffffff', 0.25);
// Lines and bars must stay visible on both light and dark backgrounds
$line_light = min(max($light, 0.48), 0.62);
$palette = array(
'light' => array(
'accent' => $primary,
'accent_hover' => eacfa_mix($primary, '#000000', 0.12),
'accent_soft' => eacfa_rgba($primary, 0.1),
'accent_ink' => eacfa_on_color($primary),
),
'dark' => array(
'accent' => $dark_accent,
'accent_hover' => eacfa_mix($dark_accent, '#ffffff', 0.12),
'accent_soft' => eacfa_rgba($primary, 0.24),
'accent_ink' => eacfa_on_color($dark_accent),
),
'series_visits' => eacfa_from_hsl($hue, $sat, $line_light),
// Page views: a hue-shifted sibling. For a grey primary, lightness tells the two lines apart instead.
'series_views' => $sat < 0.12
? eacfa_from_hsl($hue, 0, min($line_light + 0.25, 0.8))
: eacfa_from_hsl($hue + 60, $sat * 0.8, $line_light),
'subparts' => array(
eacfa_from_hsl($hue, $sat, 0.5),
eacfa_from_hsl($hue, $sat, 0.68),
eacfa_from_hsl($hue, $sat, 0.34),
eacfa_from_hsl($hue, $sat, 0.82),
),
);
// Lines on the dark background: a lighter tint of the same colour.
$palette['series_visits_dark'] = eacfa_mix($palette['series_visits'], '#ffffff', 0.25);
$palette['series_views_dark'] = eacfa_mix($palette['series_views'], '#ffffff', 0.25);
return $palette;
}
/**
* CSS custom properties for the accent, added after admin.css so they take effect.
* Only the accent group lives here; neutral and status tokens are in admin.css.
*/
function eacfa_palette_css() {
$p = eacfa_palette();
$vars = function ($t, $series) {
return '--cfa-accent:' . $t['accent'] . ';--cfa-accent-hover:' . $t['accent_hover']
. ';--cfa-accent-soft:' . $t['accent_soft'] . ';--cfa-accent-ink:' . $t['accent_ink']
. ';--cfa-chart-1:' . $series[0] . ';--cfa-chart-2:' . $series[1] . ';';
};
$light = array($p['series_visits'], $p['series_views']);
$dark = array($p['series_visits_dark'], $p['series_views_dark']);
return '.cfa-wrap,#cf_analytics_widget{' . $vars($p['light'], $light) . '}'
. 'body.cfa-theme-dark .cfa-wrap,body.cfa-theme-dark #cf_analytics_widget{' . $vars($p['dark'], $dark) . '}'
. '@media (prefers-color-scheme: dark){'
. 'body.cfa-theme-auto .cfa-wrap,body.cfa-theme-auto #cf_analytics_widget{' . $vars($p['dark'], $dark) . '}}';
}
add_filter('admin_body_class', function ($classes) {
$screen = function_exists('get_current_screen') ? get_current_screen() : null;
if ($screen && ($screen->id === 'dashboard' || strpos($screen->id, 'cf-analytics') !== false)) {
$classes .= ' cfa-theme-' . eacfa_current_theme();
if ($screen->id !== 'dashboard') {
$classes .= ' cfa-page';
}
}
return $classes;
});
/* ----------------------------------------------------------------------
* Menu: one link under Dashboard, between Home and Updates
* -------------------------------------------------------------------- */
add_action('admin_menu', function () {
add_submenu_page(
'index.php',
'Web Analytics',
'Web Analytics',
'manage_options',
'cf-analytics',
'eacfa_analytics_page',
1
);
});
/* ----------------------------------------------------------------------
* Assets + request handlers
* -------------------------------------------------------------------- */
add_action('admin_enqueue_scripts', function ($hook) {
$is_plugin_page = strpos($hook, 'cf-analytics') !== false;
if (!$is_plugin_page && $hook !== 'index.php') {
return;
}
wp_enqueue_style('eacfa-admin', EACFA_PLUGIN_URL . 'assets/css/admin.css', array(), eacfa_asset_version('assets/css/admin.css'));
wp_add_inline_style('eacfa-admin', eacfa_palette_css());
wp_enqueue_script('eacfa-chart', EACFA_PLUGIN_URL . 'assets/js/cfa-chart.js', array(), eacfa_asset_version('assets/js/cfa-chart.js'), true);
wp_enqueue_script('eacfa-live', EACFA_PLUGIN_URL . 'assets/js/cfa-live.js', array(), eacfa_asset_version('assets/js/cfa-live.js'), true);
wp_enqueue_script('eacfa-dashboard', EACFA_PLUGIN_URL . 'assets/js/cfa-dashboard.js', array('eacfa-chart', 'eacfa-live'), eacfa_asset_version('assets/js/cfa-dashboard.js'), true);
if ($is_plugin_page && eacfa_param('section') === 'settings') {
wp_enqueue_style('wp-components');
wp_enqueue_script('eacfa-color', EACFA_PLUGIN_URL . 'assets/js/cfa-color.js', array('wp-element', 'wp-components', 'wp-i18n'), eacfa_asset_version('assets/js/cfa-color.js'), true);
wp_enqueue_script('eacfa-settings', EACFA_PLUGIN_URL . 'assets/js/cfa-settings.js', array('jquery'), eacfa_asset_version('assets/js/cfa-settings.js'), true);
}
});
add_action('admin_init', function () {
if (strpos(eacfa_param('page'), 'cf-analytics') !== 0 || !current_user_can('manage_options')) {
return;
}
// Refresh data
if (eacfa_has_param('eacfa_refresh')) {
check_admin_referer('eacfa_refresh');
EACFA_Live::queue_manual();
wp_safe_redirect(remove_query_arg(array('eacfa_refresh', '_wpnonce')));
exit;
}
// Quick theme switch
if (eacfa_has_param('eacfa_theme')) {
check_admin_referer('eacfa_theme');
$saved = eacfa_set_theme(eacfa_param('eacfa_theme'));
$url = remove_query_arg(array('eacfa_theme', '_wpnonce'));
wp_safe_redirect($saved ? $url : add_query_arg('eacfa_notice', 'save_failed', $url));
exit;
}
});
/* ----------------------------------------------------------------------
* Helpers
* -------------------------------------------------------------------- */
function eacfa_sections() {
return array(
'overview' => array('Overview', 'dashicons-chart-area'),
'pages' => array('Pages', 'dashicons-admin-page'),
'referrers' => array('Referrers', 'dashicons-share'),
'countries' => array('Countries', 'dashicons-admin-site-alt3'),
'technology' => array('Technology', 'dashicons-desktop'),
'vitals' => array('Core Web Vitals', 'dashicons-performance')
);
}
function eacfa_url($args = array()) {
return EACFA_Live::dashboard_url($args);
}
function eacfa_num($n) {
return number_format_i18n((float) $n);
}
function eacfa_total($rows, $key = 'views') {
$sum = 0;
foreach ($rows as $row) {
$sum += $row[$key];
}
return $sum;
}
function eacfa_share($value, $total) {
return $total > 0 ? ($value / $total) * 100 : 0;
}
function eacfa_error($res, $inline = false) {
// Nothing cached yet: not an error. The card shows a skeleton until the background refresh fills it.
if (!empty($res['miss'])) {
if ($inline) {
// Inside a list card: a few shimmering lines fit the card shell.
echo '
';
} else {
// A whole section with nothing cached yet: show the card layout as skeletons.
eacfa_skeleton();
}
return;
}
$msg = isset($res['message']) ? $res['message'] : 'Something went wrong.';
$class = $inline ? 'cfa-empty is-error' : 'cfa-notice is-error';
echo '';
}
function eacfa_country_label($code) {
return ($code === '') ? 'Unknown' : EACFA_API::get_country_name($code);
}
/** Label callbacks shared by the list cards and tables */
function eacfa_plain_label($row) {
return '' . esc_html($row['key'] === '' ? 'Unknown' : $row['key']) . ' ';
}
function eacfa_page_label($row) {
if (!empty($row['title'])) {
return '' . esc_html($row['title']) . ' ' . esc_html($row['key']) . ' ';
}
return '' . esc_html($row['key'] === '' ? '/' : $row['key']) . ' ';
}
function eacfa_referrer_label($row) {
return '' . esc_html($row['key'] === '' ? 'Direct / none' : $row['key']) . ' ';
}
function eacfa_country_row_label($row) {
return '' . esc_html(eacfa_country_label($row['key'])) . ' ';
}
/**
* Ranked list with share bars
*/
function eacfa_list_card($title, $res, $link, $label_cb, $limit = 6) {
?>
No data for this period yet.
cell class. $rows: arrays of already-escaped cell HTML.
* Used by Pages, Referrers, Countries and the Core Web Vitals "By page" table.
*/
function eacfa_table($cols, $rows, $title = '', $shares = array()) {
$classes = array_values($cols);
?>
No data for this period yet.
$class): ?>
$cells): ?>
style="--w: %"> $html): ?>
$r) {
$share = eacfa_share($r['views'], $total);
$shares[] = round($share, 1);
$rows[] = array(
(int) ($i + 1),
wp_kses_post(call_user_func($label_cb, $r)),
esc_html(EACFA_Live::pct_text($share)),
esc_html(eacfa_num($r['views'])),
esc_html(eacfa_num($r['visits'])),
);
}
eacfa_table(array('#' => 'is-rank', $first_col => 'is-main', 'Share' => 'is-num is-pct', 'Page views' => 'is-num', 'Visits' => 'is-num'), $rows, '', $shares);
}
/**
* Placeholder shown while a section loads in the background
*/
function eacfa_skeleton() {
?>
timestamp).
*/
function eacfa_render_section($section, $range) {
switch ($section) {
case 'pages':
return eacfa_render_pages($range);
case 'referrers':
return eacfa_render_referrers($range);
case 'countries':
return eacfa_render_countries($range);
case 'technology':
return eacfa_render_technology($range);
case 'vitals':
return eacfa_render_vitals($range);
default:
return eacfa_render_overview($range);
}
}
function eacfa_updated_text($meta) {
return !empty($meta['fetched_at']) ? 'updated ' . human_time_diff($meta['fetched_at'], time()) . ' ago' : '';
}
/**
* AJAX: render a section with live fetching (used when the page had to defer it)
*/
add_action('wp_ajax_cf_analytics_section', function () {
check_ajax_referer('cf_analytics_section', 'nonce');
if (!current_user_can('manage_options') || !EACFA_API::is_configured()) {
wp_send_json_error('Unauthorized');
}
$sections = eacfa_sections();
$section = isset($_POST['section']) ? sanitize_key(wp_unslash($_POST['section'])) : 'overview';
if (!isset($sections[$section])) {
$section = 'overview';
}
$range = EACFA_Live::sanitize_range(isset($_POST['range']) ? sanitize_key(wp_unslash($_POST['range'])) : '', '7days');
ignore_user_abort(true);
ob_start();
$meta = eacfa_render_section($section, $range);
$html = ob_get_clean();
// Anything that had to be served from stale cache is refreshed by the browser right after.
$jobs = EACFA_Live::queued_jobs();
EACFA_Live::clear_queue();
wp_send_json_success(array(
'html' => $html,
'updated' => eacfa_updated_text($meta),
'jobs' => $jobs,
'refresh_nonce' => wp_create_nonce('cf_analytics_refresh')
));
});
/* ----------------------------------------------------------------------
* Page
* -------------------------------------------------------------------- */
function eacfa_analytics_page() {
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
$configured = EACFA_API::is_configured();
$settings = get_option('cf_analytics_settings', array());
$sections = eacfa_sections();
$section = eacfa_param('section', 'overview');
if ($section !== 'settings' && !isset($sections[$section])) {
$section = 'overview';
}
// Until Cloudflare is connected there is nothing to show but the setup screen.
if (!$configured) {
$section = 'settings';
}
$is_settings = ($section === 'settings');
$range = EACFA_Live::sanitize_range(eacfa_param('range'), '7days');
$ranges = EACFA_Live::ranges();
$deferred = false;
$updated = '';
$theme = eacfa_current_theme();
$args = array('section' => $section, 'range' => $range);
$to_dark_url = wp_nonce_url(eacfa_url(array_merge($args, array('eacfa_theme' => 'dark'))), 'eacfa_theme');
$to_light_url = wp_nonce_url(eacfa_url(array_merge($args, array('eacfa_theme' => 'light'))), 'eacfa_theme');
$theme_url = ($theme === 'dark') ? $to_light_url : $to_dark_url;
$theme_title = ($theme === 'dark') ? 'Switch to light mode' : (($theme === 'light') ? 'Switch to dark mode' : 'Switch between light and dark mode');
$refresh_url = wp_nonce_url(eacfa_url(array_merge($args, array('eacfa_refresh' => 1))), 'eacfa_refresh');
$settings_url = eacfa_url(array('section' => 'settings', 'range' => $range));
// Shared cache: apply a pending manual refresh, and queue anything stale for the page-load refresh.
EACFA_Live::apply_pending();
if (!$is_settings) {
EACFA_Live::queue_page_set($range);
}
$title = $is_settings ? 'Settings' : $sections[$section][0];
?>
Cloudflare Web Analytics
Couldn’t save that setting. Please try again, or check your connection in Settings.
($section === 'overview'),
'colors' => array(
'visits' => eacfa_palette()['series_visits'],
'views' => eacfa_palette()['series_views'],
),
'updated' => ($updated !== '') ? '· ' . $updated : '',
'deferred' => (bool) $deferred,
'section' => $section,
'range' => $range,
'sectionNonce' => wp_create_nonce('cf_analytics_section'),
'ajaxUrl' => admin_url('admin-ajax.php'),
));
}
/* ----------------------------------------------------------------------
* Overview
* -------------------------------------------------------------------- */
function eacfa_render_overview($range) {
$series = EACFA_Live::series($range);
$meta = array('fetched_at' => isset($series['fetched_at']) ? $series['fetched_at'] : 0);
if (empty($series['success'])) {
if (!empty($series['miss'])) {
eacfa_skeleton(); // nothing cached yet: show the card layout as skeletons
} else {
eacfa_error($series);
}
return $meta;
}
$t = $series['totals'];
$p = $series['prev'];
$vitals = EACFA_Live::vitals($range);
$load_change = ($p && $t['load_ms'] !== null && $p['load_ms'] !== null) ? EACFA_Live::change($t['load_ms'], $p['load_ms']) : null;
$chart = array(
'labels' => array_values(wp_list_pluck($series['points'], 'label')),
'visits' => array_values(wp_list_pluck($series['points'], 'visits')),
'views' => array_values(wp_list_pluck($series['points'], 'views'))
);
?>
Core Web Vitals (P75)
'LCP', 'inp' => 'INP', 'cls' => 'CLS') as $m => $label):
$v = $vitals['metrics'][$m]['p75'];
$rate = EACFA_Live::rating($m, $v);
?>
Not available yet
Traffic
Visits
Page views
'pages', 'range' => $range)), 'eacfa_page_label');
eacfa_list_card('Referrers', EACFA_Live::breakdown($range, 'refererHost', 20), eacfa_url(array('section' => 'referrers', 'range' => $range)), 'eacfa_referrer_label');
eacfa_list_card('Countries', EACFA_Live::breakdown($range, 'countryName', 30), eacfa_url(array('section' => 'countries', 'range' => $range)), 'eacfa_country_row_label');
eacfa_list_card('Browsers', EACFA_Live::breakdown($range, 'userAgentBrowser', 20), eacfa_url(array('section' => 'technology', 'range' => $range)), 'eacfa_plain_label');
?>
isset($res['fetched_at']) ? $res['fetched_at'] : 0);
}
function eacfa_render_referrers($range) {
$res = EACFA_Live::breakdown($range, 'refererHost', 50);
eacfa_table_card($res, 'Referrer', 'eacfa_referrer_label');
return array('fetched_at' => isset($res['fetched_at']) ? $res['fetched_at'] : 0);
}
function eacfa_render_countries($range) {
$res = EACFA_Live::breakdown($range, 'countryName', 100);
eacfa_table_card($res, 'Country', function ($row) {
return eacfa_country_row_label($row) . ($row['key'] !== '' ? '' . esc_html($row['key']) . ' ' : '');
});
return array('fetched_at' => isset($res['fetched_at']) ? $res['fetched_at'] : 0);
}
function eacfa_render_technology($range) {
$browsers = EACFA_Live::breakdown($range, 'userAgentBrowser', 20);
echo '';
eacfa_list_card('Browsers', $browsers, '', 'eacfa_plain_label', 12);
eacfa_list_card('Operating systems', EACFA_Live::breakdown($range, 'userAgentOS', 20), '', 'eacfa_plain_label', 12);
echo '
';
eacfa_list_card('Devices', EACFA_Live::breakdown($range, 'deviceType', 10), '', function ($row) {
return '' . esc_html($row['key'] === '' ? 'Unknown' : ucfirst($row['key'])) . ' ';
}, 10);
return array('fetched_at' => isset($browsers['fetched_at']) ? $browsers['fetched_at'] : 0);
}
/* ----------------------------------------------------------------------
* Core Web Vitals
* -------------------------------------------------------------------- */
function eacfa_render_vitals($range) {
$v = EACFA_Live::vitals($range);
$meta = array('fetched_at' => isset($v['fetched_at']) ? $v['fetched_at'] : 0);
if (empty($v['success'])) {
eacfa_error($v);
return $meta;
}
$info = array(
'lcp' => array('Largest Contentful Paint', 'LCP', 'Render time of the largest image or text block visible in the viewport.'),
'inp' => array('Interaction to Next Paint', 'INP', 'How quickly the page responds to clicks, taps and key presses.'),
'cls' => array('Cumulative Layout Shift', 'CLS', 'The largest burst of unexpected layout shifts during the life of a page.')
);
$percentiles = array('p50' => 'P50', 'p75' => 'P75', 'p90' => 'P90', 'p99' => 'P99');
$subpart_colors = eacfa_palette()['subparts'];
foreach ($info as $metric => $text):
$m = $v['metrics'][$metric];
$overall = EACFA_Live::rating($metric, $m['p75']);
?>
()
at P75
'Good', 'needs' => 'Needs improvement', 'poor' => 'Poor') as $k => $label): ?>
$label):
$rate = EACFA_Live::rating($metric, $m[$key]);
?>
Sub-parts (P75)
$ms): ?>
$ms): ?>
ms
By page (P75)
Per-page vitals aren’t available right now.
' . esc_html($r['key'] === '' ? '/' : $r['key']) . '', esc_html(eacfa_num($r['count'])));
foreach (array('lcp', 'inp', 'cls') as $metric) {
$rate = EACFA_Live::rating($metric, $r[$metric]);
$cells[] = ' ' . esc_html(EACFA_Live::vital_text($metric, $r[$metric]));
}
$rows[] = $cells;
}
eacfa_table(array('Page' => 'is-main', 'Samples' => 'is-num', 'LCP' => 'is-num', 'INP' => 'is-num', 'CLS' => 'is-num'), $rows, 'By page (P75) ');
}
return $meta;
}