Implémentation du CamSoda Stats Widget
Introduction à l’outil Live Stats CamSoda.
Récemment, j’ai travaillé sur la publication d’un shortcode WP permettant d’exploiter le feed JSON public de CamSoda afin d’afficher à l’écran des statistiques en temps réel de l’activité de la plateforme.
Vous pouvez voir comment ceci est exécuté sur la page suivante : Statistiques en temps réel de la plateforme CamSoda. Cet outil a été développé fonctionnellement par moi-même, et l’IA a réalisé toute la partie de coding. Je trouve les résultats assez intéressants.
La documentation est rédigée en anglais pour être la plus accessible possible.
CamSoda Live Stats – WordPress Shortcode Documentation
1. What this shortcode does
[camsoda_stats] is a plug-and-play WordPress shortcode that displays live statistics from CamSoda, using their public JSON feed:
- Pie chart: gender breakdown of currently online models
- Table: top models by viewers (thumb, username, viewers, followers)
- Bar chart: distribution of viewer counts (0–50, 51–100, …)
- Tag cloud: most frequent tags among online models (with links to CamSoda tag pages)
- Optional text explanations under each block (multilingual)
- Optional credit link to SpritzCams (SEO-friendly, configurable
rel=)
All rendering is done via Chart.js (CDN) and plain HTML, no extra plugin required.
2. Installation
- Create a PHP file in your WordPress install, for example:
wp-content/mu-plugins/camsoda-stats.php - Paste the provided PHP code of
camsoda_stats_shortcode()into that file.
(MU-plugins are loaded automatically; no activation step.) - In any post, page or widget, insert the shortcode:
[camsoda_stats]The widget will call CamSoda’s JSON feed and render the charts.
3. Basic usage
Minimal example:
[camsoda_stats id="YOUR_AFFILIATE_ID" cmp="my-site" lang="en"]Typical setup for a French webmaster:
[camsoda_stats id="hklein" cmp="forum-xyz" lang="fr" show_text="1"]4. Shortcode parameters
All parameters are optional and have sensible defaults.
| Parameter | Type | Default | Description |
|---|---|---|---|
id | string | hklein | CamSoda affiliate ID used in all outbound links (?id=...). Use your own ID. |
cmp | string | feed | Campaign / source label (cmp= query parameter). Useful to track where clicks come from (forum name, site, etc.). |
index | int | 1 | Index passed to the CamSoda JSON feed. Normally you can leave this at 1. |
sound | yes/no | no | Controls sound= in the CamSoda URLs. Use sound="yes" if you prefer sound enabled by default. |
top_n | int | 10 | Number of rows in the “Top models by viewers” table. Clamped between 1 and 50. |
tag_limit | int | 50 | Maximum number of tags shown in the tag cloud. Clamped between 1 and 200. |
cache_ttl | int (s) | 60 | Cache duration in seconds, using WordPress transients. 0 disables caching (not recommended if you have traffic). |
show_footer | 1/0 | 1 | Whether to display the “Stats provided by SpritzCams” footer link. |
footer_rel | string | noopener noreferrer | Value of the rel="..." attribute on the SpritzCams link. You can add nofollow here if you want. |
show_text | 1/0 | 1 | Shows or hides the explanatory paragraphs under each chart/table. |
lang | string | en | Language for all texts: fr, en, es, de. A wrong value falls back to en. |
5. Language handling
The following elements are automatically translated according to lang:
- Main title (“CamSoda live stats”, “Statistiques CamSoda en direct”, …)
- All
<h3>headings - Table column headers (Thumb / Username / Viewers / Followers…)
- The “Stats provided by…” footer prefix
- Error messages
- Explanatory paragraphs under each section (gender, top models, viewers distribution, tag cloud)
- Dataset label of the bar chart (“Number of models”, “Nombre de modèles”, etc.)
- Gender labels in the pie chart (Female / Male / Trans / Couples, etc.)
Examples:
[camsoda_stats id="myid" cmp="myblog" lang="en"]
[camsoda_stats id="miid" cmp="foro-abc" lang="es"]
[camsoda_stats id="meinid" cmp="board-de" lang="de"]
[camsoda_stats id="monid" cmp="forum-fr" lang="fr"]If you want only the visuals and plan to write your own text around them:
[camsoda_stats id="myid" cmp="myblog" lang="en" show_text="0"]6. SEO & outbound links
The widget generates three kinds of links:
- Links to CamSoda models (thumb + username in the top table)
- Include your
id,cmpandsoundparameters:https://www.camsoda.com/USERNAME?id=ID&type=REV&cmp=CMP&sound=... rel="noopener noreferrer"for security.- These links are not controlled by
footer_rel.
- Include your
- Links in the tag cloud
- Point to CamSoda tag pages:
https://www.camsoda.com/girls/tag/TAG-cams?... - Same
id,cmp,soundhandling, samerel="noopener noreferrer".
- Point to CamSoda tag pages:
- Footer link to SpritzCams
- Shown only if
show_footer="1". - You control its
relattribute viafooter_rel.
- Shown only if
Examples:
; Dofollow, but with security
[camsoda_stats id="myid" cmp="myblog" footer_rel="noopener noreferrer"]
; Explicit nofollow
[camsoda_stats id="myid" cmp="myblog" footer_rel="noopener noreferrer nofollow"]
; Completely custom rel
[camsoda_stats id="myid" cmp="myblog" footer_rel="nofollow sponsored"]If you leave footer_rel empty (footer_rel=""), no rel attribute is added on the SpritzCams link.
7. Performance and caching
The shortcode calls CamSoda’s JSON feed. To avoid hitting the API too often, the result can be cached in a WordPress transient:
cache_ttl="60"→ cache for 60 secondscache_ttl="300"→ cache for 5 minutescache_ttl="0"→ no caching (each page view triggers a remote call)
Recommended for most sites:
[camsoda_stats id="myid" cmp="myblog" cache_ttl="60"]8. Styling
The shortcode comes with minimal inline styles (border radius on thumbs, basic layout). You can override them in your theme stylesheet using these hooks:
- Wrapper:
.camsoda-stats-wrapper - Explanation paragraphs:
.camsoda-stats-explain - Footer:
.camsoda-stats-footer - Table:
.camsoda-stats-table
Example:
.camsoda-stats-wrapper h3 {
margin-top: 1.5em;
}
.camsoda-stats-table {
width: 100%;
border-collapse: collapse;
}
.camsoda-stats-table th,
.camsoda-stats-table td {
padding: 6px 8px;
}
9. Copy/Paste the Complete Plugin Code
<?php
/**
* CamSoda Stats — shortcode public multilingue
*
* Shortcode :
* [camsoda_stats id="hklein" cmp="feed" sound="no"
* top_n="10" tag_limit="50" cache_ttl="60"
* show_footer="1" footer_rel="noopener noreferrer"
* show_text="1" lang="en"]
*/
if (!defined('ABSPATH')) exit;
function camsoda_stats_shortcode( $atts = [] ) {
// ================== Paramètres du shortcode ==================
$atts = shortcode_atts(
[
'id' => 'hklein', // ID affilié CamSoda
'cmp' => 'feed', // campagne / source
'index' => 1, // index du feed
'top_n' => 10, // taille du top modèles
'tag_limit' => 50, // nb max de tags dans le nuage
'cache_ttl' => 60, // durée de cache (0 = pas de cache)
'show_footer' => '1', // afficher la mention SpritzCams ?
'sound' => 'no', // "yes" ou "no"
'footer_rel' => 'noopener noreferrer', // rel=... pour le lien SpritzCams
'show_text' => '1', // afficher les textes explicatifs ? "1"/"0"/"yes"/"no"
'lang' => 'en', // fr, en, es, de
],
$atts,
'camsoda_stats'
);
$top_n = max(1, min(50, intval($atts['top_n'])));
$tag_limit = max(1, min(200, intval($atts['tag_limit'])));
$cache_ttl = max(0, intval($atts['cache_ttl']));
// Normalisation sound
$sound = ( strtolower($atts['sound']) === 'yes' ) ? 'yes' : 'no';
// footer_rel
$footer_rel = trim( (string) $atts['footer_rel'] );
// show_text
$show_text = true;
$st = strtolower( trim( (string) $atts['show_text'] ) );
if ( $st === '0' || $st === 'no' || $st === 'false' ) {
$show_text = false;
}
// lang
$lang = strtolower( trim( (string) $atts['lang'] ) );
if ( ! in_array( $lang, ['fr','en','es','de'], true ) ) {
$lang = 'en';
}
// ================== Textes multilingues ==================
$gender_label_map = [];
switch ($lang) {
case 'fr':
$gender_label_map = [
'f' => 'Femmes',
'm' => 'Hommes',
't' => 'Trans',
'c' => 'Couples',
];
$txt_main_title = "Statistiques CamSoda en direct";
$txt_total_online_label = "Nombre total de modèles en ligne :";
$txt_h3_gender = "Répartition par genre";
$txt_h3_top = "Top modèles par spectateurs";
$txt_h3_viewers = "Distribution du nombre de spectateurs";
$txt_h3_tags = "Nuage de tags";
$txt_col_thumb = "Vignette";
$txt_col_username = "Pseudo";
$txt_col_viewers = "Spectateurs";
$txt_col_followers = "Abonnés";
$txt_footer_prefix = "Stats fournies par";
$txt_chart_viewers_label = "Nombre de modèles";
$txt_err_api = "Impossible de récupérer les données CamSoda.";
$txt_err_invalid = "Données JSON invalides ou format inattendu.";
$txt_err_none = "Aucun modèle en ligne pour le moment.";
$txt_gender = "Ce graphique montre la répartition des modèles actuellement connectés sur CamSoda selon leur genre. Chaque part du camembert correspond à une catégorie : modèles féminins, masculins, trans ou couples. Plus une part est grande, plus ce type de modèles est présent en ce moment sur la plateforme. C’est une photographie en temps réel de l’offre disponible : utile pour voir en un coup d’œil si le site est plutôt dominé par les camgirls, les couples, ou si la scène est plus équilibrée. Les proportions peuvent varier fortement selon l’heure de la journée et le jour de la semaine.";
$txt_top = "Ce tableau liste les modèles les plus regardés en ce moment sur CamSoda. La colonne « Viewers » indique le nombre de spectateurs connectés dans leur salle au moment de la mesure : plus le chiffre est élevé, plus la room est populaire. La colonne « Followers » montre combien d’utilisateurs ont choisi de suivre ce modèle sur le long terme. Le lien sur le pseudo et la miniature permet d’ouvrir directement le show dans une nouvelle fenêtre. C’est un bon moyen d’identifier les stars du moment, mais aussi de repérer les profils qui fidélisent réellement leur audience.";
$txt_viewers = "Ce graphique regroupe les modèles par tranches de spectateurs : 0–50, 51–100, 101–150, etc. Chaque barre représente le nombre de rooms qui se trouvent dans cette zone de popularité. Si la majorité des modèles est dans les petites tranches, cela signifie que l’audience est très dispersée. Si au contraire les barres les plus hautes se situent dans les grosses tranches, c’est que quelques rooms captent l’essentiel du trafic. Ce type de vue permet de comprendre si CamSoda fonctionne comme un écosystème très concentré autour de quelques stars ou si la plateforme laisse une vraie place aux modèles de taille moyenne.";
$txt_tags = "Ce nuage de tags met en avant les thèmes et catégories les plus fréquents parmi les modèles actuellement en ligne. Chaque mot représente un tag utilisé sur CamSoda, par exemple un style, un type de show, un fétiche ou une caractéristique physique. Plus le mot est grand, plus il apparaît souvent dans les profils en ce moment. Cliquer sur un tag ouvre une page CamSoda regroupant les modèles associés. C’est une façon visuelle de voir quelles tendances dominent l’instant : cosplay, couples amateurs, shows interactifs ou catégories plus classiques, selon l’humeur de l’audience et l’heure de la journée.";
break;
case 'es':
$gender_label_map = [
'f' => 'Mujeres',
'm' => 'Hombres',
't' => 'Trans',
'c' => 'Parejas',
];
$txt_main_title = "Estadísticas en directo de CamSoda";
$txt_total_online_label = "Número total de modelos conectados:";
$txt_h3_gender = "Distribución por género";
$txt_h3_top = "Top de modelos por espectadores";
$txt_h3_viewers = "Distribución del número de espectadores";
$txt_h3_tags = "Nube de etiquetas";
$txt_col_thumb = "Miniatura";
$txt_col_username = "Usuario";
$txt_col_viewers = "Espectadores";
$txt_col_followers = "Seguidores";
$txt_footer_prefix = "Estadísticas ofrecidas por";
$txt_chart_viewers_label = "Número de modelos";
$txt_err_api = "No se pueden recuperar los datos de CamSoda.";
$txt_err_invalid = "Datos JSON no válidos o formato inesperado.";
$txt_err_none = "No hay modelos conectados en este momento.";
$txt_gender = "Este gráfico muestra la distribución de los modelos conectados en CamSoda según su género. Cada porción del pastel corresponde a una categoría: modelos femeninos, masculinos, trans o parejas. Cuanto más grande es una porción, más presente está ese tipo de modelo en la plataforma en este momento. Es una fotografía en tiempo real de la oferta disponible y permite ver de un vistazo si el sitio está dominado por camgirls, parejas o si la escena está más equilibrada. Las proporciones pueden cambiar mucho según la hora del día y el día de la semana.";
$txt_top = "Esta tabla presenta los modelos más vistos en este momento en CamSoda. La columna « Viewers » indica el número de espectadores conectados en la sala en el momento de la medición: cuanto mayor es la cifra, más popular es el show. La columna « Followers » muestra cuántos usuarios han decidido seguir a este modelo a largo plazo. El enlace en el nombre de usuario y la miniatura permite abrir la sala directamente en una nueva ventana. Es una buena forma de identificar las estrellas del momento y también los perfiles capaces de fidelizar realmente a su audiencia.";
$txt_viewers = "Este gráfico agrupa los modelos por rangos de espectadores: 0–50, 51–100, 101–150, etc. Cada barra representa el número de salas que se sitúan dentro de ese nivel de popularidad. Si la mayoría de los modelos se concentran en los rangos bajos, significa que la audiencia está muy distribuida. Si, por el contrario, las barras más altas aparecen en los rangos grandes, unas pocas salas concentran casi todo el tráfico. Esta vista ayuda a entender si CamSoda funciona como un sistema de grandes estrellas o si ofrece espacio real para modelos de tamaño medio.";
$txt_tags = "Esta nube de etiquetas muestra los temas y categorías más frecuentes entre los modelos conectados. Cada palabra representa un tag utilizado en CamSoda, por ejemplo un estilo, un tipo de show, un fetiche o una característica física. Cuanto más grande aparece la palabra, más presente está en los perfiles en este momento. Al hacer clic en un tag se abre una página de CamSoda con los modelos asociados. Es una forma visual de descubrir qué tendencias dominan ahora mismo: cosplay, parejas amateurs, shows interactivos u opciones más clásicas, según el gusto de la audiencia.";
break;
case 'de':
$gender_label_map = [
'f' => 'Frauen',
'm' => 'Männer',
't' => 'Trans',
'c' => 'Paare',
];
$txt_main_title = "CamSoda Live-Statistiken";
$txt_total_online_label = "Gesamtzahl der aktuell online Models:";
$txt_h3_gender = "Verteilung nach Geschlecht";
$txt_h3_top = "Top-Models nach Zuschauern";
$txt_h3_viewers = "Verteilung der Zuschauerzahlen";
$txt_h3_tags = "Schlagwortwolke";
$txt_col_thumb = "Vorschau";
$txt_col_username = "Benutzername";
$txt_col_viewers = "Zuschauer";
$txt_col_followers = "Follower";
$txt_footer_prefix = "Statistiken bereitgestellt von";
$txt_chart_viewers_label = "Anzahl der Models";
$txt_err_api = "Die CamSoda-Daten können nicht abgerufen werden.";
$txt_err_invalid = "Ungültige JSON-Daten oder unerwartetes Format.";
$txt_err_none = "Derzeit sind keine Models online.";
$txt_gender = "Dieses Diagramm zeigt die Verteilung der aktuell auf CamSoda aktiven Models nach Geschlecht. Jeder Abschnitt des Kreisdiagramms steht für eine Kategorie: weibliche Models, männliche Models, Trans-Models oder Paare. Je größer ein Abschnitt ist, desto stärker ist dieser Typ im Moment auf der Plattform vertreten. Es handelt sich um eine Momentaufnahme des Angebots und sie hilft dabei, auf einen Blick zu sehen, ob CamSoda eher von Camgirls, Paaren oder einer ausgewogenen Mischung dominiert wird. Die Anteile können sich je nach Tageszeit und Wochentag deutlich verändern.";
$txt_top = "Diese Tabelle listet die aktuell beliebtesten Models auf CamSoda auf. Die Spalte „Viewers“ zeigt, wie viele Zuschauer zum Zeitpunkt der Messung im Raum sind: je höher der Wert, desto populärer die Show. Die Spalte „Followers“ zeigt, wie viele Nutzer diesem Model langfristig folgen. Über den Link auf den Benutzernamen und das Vorschaubild lässt sich der Raum direkt in einem neuen Fenster öffnen. So lassen sich die Stars des Moments leicht erkennen – und auch jene Profile, die es schaffen, eine treue Stammkundschaft aufzubauen.";
$txt_viewers = "Dieses Diagramm gruppiert die Models nach Zuschauerbereichen: 0–50, 51–100, 101–150 usw. Jeder Balken steht für die Anzahl der Räume in dieser Popularitätszone. Befinden sich die meisten Models in den kleinen Bereichen, ist das Publikum stark verteilt. Konzentrieren sich die höchsten Balken hingegen in den großen Bereichen, bündeln einige wenige Räume den Großteil des Traffics. Diese Ansicht hilft zu verstehen, ob CamSoda wie ein klassisches Starsystem funktioniert oder ob es viel Raum für mittelgroße Models gibt.";
$txt_tags = "Diese Schlagwortwolke hebt die Themen und Kategorien hervor, die bei den aktuell aktiven Models am häufigsten vorkommen. Jedes Wort steht für einen auf CamSoda verwendeten Tag, zum Beispiel ein Stil, eine Art von Show, ein Fetisch oder ein körperliches Merkmal. Je größer ein Wort dargestellt wird, desto häufiger taucht es in den Profilen auf. Ein Klick auf ein Schlagwort öffnet eine CamSoda-Seite mit den zugehörigen Models. So lässt sich visuell erkennen, welche Trends gerade dominieren – etwa Cosplay, Amateurpaare, interaktive Shows oder eher klassische Kategorien.";
break;
case 'en':
default:
$gender_label_map = [
'f' => 'Female',
'm' => 'Male',
't' => 'Trans',
'c' => 'Couples',
];
$txt_main_title = "CamSoda live stats";
$txt_total_online_label = "Total number of models online:";
$txt_h3_gender = "Gender breakdown";
$txt_h3_top = "Top models by viewers";
$txt_h3_viewers = "Viewer count distribution";
$txt_h3_tags = "Tag cloud";
$txt_col_thumb = "Thumb";
$txt_col_username = "Username";
$txt_col_viewers = "Viewers";
$txt_col_followers = "Followers";
$txt_footer_prefix = "Stats provided by";
$txt_chart_viewers_label = "Number of models";
$txt_err_api = "Unable to fetch CamSoda data.";
$txt_err_invalid = "Invalid JSON data or unexpected format.";
$txt_err_none = "No model online at the moment.";
$txt_gender = "This chart shows how the models currently online on CamSoda are distributed by gender. Each slice of the pie represents a category: female models, male models, trans models or couples. The larger the slice, the more present that type of model is on the platform at this moment. It is a real-time snapshot of the available supply and gives you a quick view of whether the site is dominated by camgirls, couples or a more balanced mix. The proportions can change a lot depending on the time of day and the day of the week.";
$txt_top = "This table lists the most watched models on CamSoda right now. The “Viewers” column shows how many people are currently in the room: the higher the number, the more popular the show is. The “Followers” column indicates how many users decided to follow this model over the long term. Clicking on the username or thumbnail opens the show in a new window. It is a useful way to spot the current stars, but also to see which profiles are really able to build and keep a loyal audience over time.";
$txt_viewers = "This chart groups models by viewer ranges: 0–50, 51–100, 101–150, and so on. Each bar represents how many rooms fall into that popularity band. If most models are in the smaller ranges, the audience is very spread out. If the tallest bars appear in the higher ranges, a handful of rooms are capturing most of the traffic. This view helps you understand whether CamSoda behaves like a classic star system, where a few shows dominate, or whether there is real space for medium-sized models to attract an audience.";
$txt_tags = "This tag cloud highlights the most common themes and categories among the models currently online. Each word represents a tag used on CamSoda, for example a style, type of show, fetish or physical characteristic. The larger the word, the more frequently it appears in model profiles right now. Clicking on a tag opens a CamSoda page listing the models associated with it. It is a visual way to see which trends dominate at this moment: cosplay, amateur couples, interactive shows or more classic categories, depending on the mood and tastes of the audience.";
break;
}
// ================== Cache (transient) ==================
$cache_key = 'camsoda_stats_' . md5( serialize( $atts ) );
$models = false;
if ( $cache_ttl > 0 ) {
$models = get_transient( $cache_key );
}
if ( false === $models ) {
// ================== Appel API CamSoda ==================
$json_url = sprintf(
'https://feed.camsoda.com/api/v1/browse/online_embed?id=%s&cmp=%s&index=%d',
rawurlencode($atts['id']),
rawurlencode($atts['cmp']),
intval($atts['index'])
);
$response = wp_remote_get( $json_url, [ 'timeout' => 10 ] );
if ( is_wp_error( $response ) ) {
return '<p>' . esc_html( $txt_err_api ) . '</p>';
}
$body = wp_remote_retrieve_body( $response );
$data = json_decode( trim( $body ), true );
if ( ! $data || ! isset( $data['results'] ) || ! is_array( $data['results'] ) ) {
return '<p>' . esc_html( $txt_err_invalid ) . '</p>';
}
$models = $data['results'];
if ( $cache_ttl > 0 ) {
set_transient( $cache_key, $models, $cache_ttl );
}
}
if ( empty( $models ) || ! is_array( $models ) ) {
return '<p>' . esc_html( $txt_err_none ) . '</p>';
}
// ================== IDs uniques ==================
$uid = uniqid('csstats_');
$gender_canvas_id = 'genderChart_' . $uid;
$viewer_canvas_id = 'viewerDistChart_' . $uid;
$wordcloud_id = 'wordCloud_' . $uid;
$total_models = count($models);
// ================== Répartition par genre ==================
$gender_labels = $gender_label_map;
$gender_counts = [];
foreach ( $models as $m ) {
if ( empty($m['gender']) ) continue;
$g = $m['gender'];
$label = $gender_labels[ $g ] ?? ucfirst( $g );
$gender_counts[ $label ] = ( $gender_counts[ $label ] ?? 0 ) + 1;
}
// ================== Top N par viewers ==================
usort( $models, function( $a, $b ) {
return intval($b['viewers'] ?? 0) - intval($a['viewers'] ?? 0);
});
$top_models = array_slice( $models, 0, $top_n );
// ================== Distribution viewers ==================
$viewer_buckets = [
"0–50" => 0,
"51–100" => 0,
"101–150" => 0,
"151–200" => 0,
"201–500" => 0,
"501+" => 0
];
foreach ( $models as $m ) {
$v = intval( $m['viewers'] ?? 0 );
if ( $v <= 50 ) $viewer_buckets["0–50"]++;
elseif ( $v <= 100 ) $viewer_buckets["51–100"]++;
elseif ( $v <= 150 ) $viewer_buckets["101–150"]++;
elseif ( $v <= 200 ) $viewer_buckets["151–200"]++;
elseif ( $v <= 500 ) $viewer_buckets["201–500"]++;
else $viewer_buckets["501+"]++;
}
// ================== Nuage de tags top N ==================
$tag_counts = [];
foreach ( $models as $m ) {
if ( ! empty($m['tags']) && is_array($m['tags']) ) {
foreach ( $m['tags'] as $tag ) {
$t = strtolower( trim( $tag ) );
if ( $t === '' ) continue;
$tag_counts[ $t ] = ( $tag_counts[ $t ] ?? 0 ) + 1;
}
}
}
arsort( $tag_counts );
$tag_counts = array_slice( $tag_counts, 0, $tag_limit, true );
if ( ! empty($tag_counts) ) {
$min_count = min( $tag_counts );
$max_count = max( $tag_counts );
} else {
$min_count = 0;
$max_count = 0;
}
$tags_for_js = [];
foreach ( $tag_counts as $tag => $count ) {
$tags_for_js[] = [ 'tag' => $tag, 'count' => $count ];
}
static $chartjs_loaded = false;
ob_start();
?>
<div class="camsoda-stats-wrapper">
<h3><?php echo esc_html( $txt_main_title ); ?></h3>
<p><strong><?php echo esc_html( $txt_total_online_label ); ?></strong> <?php echo intval($total_models); ?></p>
<h3><?php echo esc_html( $txt_h3_gender ); ?></h3>
<canvas id="<?php echo esc_attr($gender_canvas_id); ?>" width="400" height="300"></canvas>
<?php if ( $show_text ) : ?>
<p class="camsoda-stats-explain camsoda-stats-explain-gender">
<?php echo esc_html( $txt_gender ); ?>
</p>
<?php endif; ?>
<h3><?php echo esc_html( $txt_h3_top ); ?> (Top <?php echo intval($top_n); ?>)</h3>
<table class="camsoda-stats-table" border="1" cellpadding="5" cellspacing="0">
<thead>
<tr>
<th><?php echo esc_html( $txt_col_thumb ); ?></th>
<th><?php echo esc_html( $txt_col_username ); ?></th>
<th><?php echo esc_html( $txt_col_viewers ); ?></th>
<th><?php echo esc_html( $txt_col_followers ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $top_models as $m ) :
$username = $m['username'] ?? '';
$viewers = intval( $m['viewers'] ?? 0 );
$followers = intval( $m['followers'] ?? 0 );
$thumb_url = '';
if ( ! empty($m['thumb']) ) {
$thumb_url = $m['thumb'];
if ( strpos($thumb_url, '//') === 0 ) {
$thumb_url = 'https:' . $thumb_url;
}
}
$user_link = sprintf(
'https://www.camsoda.com/%s?id=%s&type=REV&cmp=%s&sound=%s',
rawurlencode( $username ),
rawurlencode( $atts['id'] ),
rawurlencode( $atts['cmp'] ),
rawurlencode( $sound )
);
?>
<tr>
<td>
<?php if ( $thumb_url ) : ?>
<a href="<?php echo esc_url( $user_link ); ?>" target="_blank" rel="noopener noreferrer">
<img src="<?php echo esc_url( $thumb_url ); ?>" alt="<?php echo esc_attr( $username ); ?>" width="80" style="border-radius:6px;" />
</a>
<?php else : ?>
-
<?php endif; ?>
</td>
<td>
<a href="<?php echo esc_url( $user_link ); ?>" target="_blank" rel="noopener noreferrer">
<?php echo esc_html( $username ); ?>
</a>
</td>
<td><?php echo $viewers; ?></td>
<td><?php echo $followers; ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php if ( $show_text ) : ?>
<p class="camsoda-stats-explain camsoda-stats-explain-top">
<?php echo esc_html( $txt_top ); ?>
</p>
<?php endif; ?>
<h3><?php echo esc_html( $txt_h3_viewers ); ?></h3>
<canvas id="<?php echo esc_attr($viewer_canvas_id); ?>" width="400" height="300"></canvas>
<?php if ( $show_text ) : ?>
<p class="camsoda-stats-explain camsoda-stats-explain-viewers">
<?php echo esc_html( $txt_viewers ); ?>
</p>
<?php endif; ?>
<h3><?php echo esc_html( $txt_h3_tags ); ?> (Top <?php echo intval($tag_limit); ?>)</h3>
<div id="<?php echo esc_attr($wordcloud_id); ?>"
style="width:600px;height:600px;margin:auto;position:relative;border-radius:50%;overflow:hidden;">
</div>
<?php if ( $show_text ) : ?>
<p class="camsoda-stats-explain camsoda-stats-explain-tags">
<?php echo esc_html( $txt_tags ); ?>
</p>
<?php endif; ?>
<?php if ( $atts['show_footer'] === '1' ) : ?>
<div class="camsoda-stats-footer" style="margin-top:1.5em;font-size:0.9em;opacity:0.8;text-align:center;">
<?php echo esc_html( $txt_footer_prefix ); ?>
<a href="https://spritzcams.com"
target="_blank"
<?php if ( $footer_rel !== '' ) : ?>
rel="<?php echo esc_attr( $footer_rel ); ?>"
<?php endif; ?>>
SpritzCams – Camgirls Reviews and CamSoda Online Statistics
</a>
</div>
<?php endif; ?>
</div>
<?php if ( ! $chartjs_loaded ) : ?>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<?php $chartjs_loaded = true; ?>
<?php endif; ?>
<script>
(function(){
const genderLabels = <?php echo wp_json_encode( array_keys( $gender_counts ) ); ?>;
const genderCounts = <?php echo wp_json_encode( array_values( $gender_counts ) ); ?>;
const viewerLabels = <?php echo wp_json_encode( array_keys( $viewer_buckets ) ); ?>;
const viewerCounts = <?php echo wp_json_encode( array_values( $viewer_buckets ) ); ?>;
const tags = <?php echo wp_json_encode( $tags_for_js ); ?>;
const maxCount = <?php echo (int) $max_count; ?>;
const minCount = <?php echo (int) $min_count; ?>;
const viewerDatasetLabel = <?php echo wp_json_encode( $txt_chart_viewers_label ); ?>;
// Camembert genres
const gEl = document.getElementById('<?php echo esc_js($gender_canvas_id); ?>');
if (gEl && window.Chart) {
new Chart(gEl.getContext('2d'), {
type: 'pie',
data: {
labels: genderLabels,
datasets: [{
data: genderCounts
}]
},
options: { responsive: true }
});
}
// Histogramme viewers
const vEl = document.getElementById('<?php echo esc_js($viewer_canvas_id); ?>');
if (vEl && window.Chart) {
new Chart(vEl.getContext('2d'), {
type: 'bar',
data: {
labels: viewerLabels,
datasets: [{
label: viewerDatasetLabel,
data: viewerCounts
}]
},
options: {
responsive: true,
scales: {
y: { beginAtZero: true }
}
}
});
}
// Nuage de tags
const container = document.getElementById('<?php echo esc_js($wordcloud_id); ?>');
if (!container || !tags.length || maxCount === 0) return;
const minSize = 14, maxSize = 36;
const colors = ['#e6194b','#3cb44b','#ff4500','#1e90ff','#32cd32','#ff1493','#ffa500','#8b00ff','#00ced1','#ff6347','#ffd700','#00fa9a','#dc143c','#00bfff','#ff69b4'];
const cx = container.clientWidth / 2;
const cy = container.clientHeight / 2;
const radius = Math.min(cx, cy) * 0.9;
const placed = [];
function overlaps(r1,r2){
return !(r2.left>r1.right || r2.right<r1.left || r2.top>r1.bottom || r2.bottom<r1.top);
}
tags.forEach(function(item, index){
const size = minSize + (item.count - minCount) * (maxSize - minSize) / Math.max(1, maxCount - minCount);
const a = document.createElement('a');
a.href = "https://www.camsoda.com/girls/tag/" + encodeURIComponent(item.tag) +
"-cams?id=<?php echo esc_js($atts['id']); ?>&type=REV&cmp=" +
encodeURIComponent("<?php echo esc_js($atts['cmp']); ?>") +
"&sound=<?php echo esc_js($sound); ?>";
a.target = "_blank";
a.rel = "noopener noreferrer";
a.style.position = 'absolute';
a.style.fontSize = size + 'px';
a.style.color = colors[Math.floor(Math.random()*colors.length)];
a.style.whiteSpace = 'nowrap';
a.textContent = item.tag;
container.appendChild(a);
let angle = Math.random() * 2 * Math.PI;
let rStep = 2;
let r = 10;
let placedRect;
let tries = 0;
do {
const x = cx + r * Math.cos(angle);
const y = cy + r * Math.sin(angle);
a.style.left = x + 'px';
a.style.top = y + 'px';
const rect = {
top: y - a.offsetHeight / 2,
left: x - a.offsetWidth / 2,
bottom: y + a.offsetHeight / 2,
right: x + a.offsetWidth / 2
};
placedRect = rect;
if (!placed.some(function(r2){ return overlaps(r2, rect); })) {
break;
}
r += rStep;
angle += 0.3;
if (r > radius) r = radius;
tries++;
} while (tries < 1000);
placed.push(placedRect);
});
})();
</script>
<?php
return ob_get_clean();
}
add_shortcode( 'camsoda_stats', 'camsoda_stats_shortcode' );
