Initial commit

This commit is contained in:
s4luorth
2026-02-07 13:08:18 +01:00
commit 68f683625d
6 changed files with 1024 additions and 0 deletions

28
assets/css/rr-charts.css Normal file
View File

@@ -0,0 +1,28 @@
.rr-chart-wrap {
margin: 0 auto;
padding: 16px;
background: #F6F9FC;
border-radius: 8px;
box-sizing: border-box;
}
.rr-chart-wrap canvas {
width: 100% !important;
}
/* Wenn ein Elementor-Container zwei Shortcodes nebeneinander hat (50/50 Columns) */
.elementor-column[data-col="50"] .rr-chart-wrap,
.elementor-col-50 .rr-chart-wrap {
width: 100% !important;
max-width: 100% !important;
}
/* Responsive: unter 768px */
@media (max-width: 768px) {
.rr-chart-wrap {
width: 100% !important;
max-width: 100% !important;
padding: 8px;
border-radius: 4px;
}
}

20
assets/js/chart.min.js vendored Normal file

File diff suppressed because one or more lines are too long

7
assets/js/chartjs-datalabels.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,276 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class RR_Charts {
private $data;
private static $counter = 0;
// RR Farbpalette
private $colors = [
'primary' => '#2F4858',
'accent' => '#C5422A',
'hover' => '#B33822',
'bg' => '#F6F9FC',
'geoeffnet' => '#2F7A4D',
'geschlossen'=> '#C5422A',
'neuer_betreiber' => '#D4922A',
];
// Abstufungen der Primärfarbe
private $primary_shades = [
'#2F4858', '#3D5A6E', '#4B6D84', '#5A7F9A',
'#6892B0', '#77A4C6', '#86B7DC', '#9ECAEB',
'#B5D7F0', '#CCE4F5', '#E3F1FA', '#F0F7FC',
'#2A4050', '#3E5C72', '#527894', '#6694B6',
'#7AB0D8',
];
public function __construct( RR_Data $data ) {
$this->data = $data;
}
private function get_id() {
return 'rr-chart-' . ( ++self::$counter ) . '-' . wp_rand( 1000, 9999 );
}
private function enqueue() {
wp_enqueue_script( 'chartjs' );
wp_enqueue_script( 'chartjs-datalabels' );
wp_enqueue_style( 'rr-charts' );
}
private function wrap( $canvas_id, $js, $height = '400px', $mobile_height = null ) {
if ( null === $mobile_height ) {
$mobile_height = $height;
}
$style = sprintf(
'width:100%%;max-width:100%%;height:%s;position:relative;',
esc_attr( $height )
);
$fn = str_replace( '-', '_', $canvas_id );
return sprintf(
'<div class="rr-chart-wrap" id="wrap_%s" style="%s"><canvas id="%s"></canvas></div>
<script>
(function(){
var attempts=0;
var mob=window.innerWidth<768;
if(mob){var w=document.getElementById("wrap_%s");if(w)w.style.height="%s";}
function %s(){
var ctx=document.getElementById("%s");
if(!ctx){if(attempts++<50){setTimeout(%s,300);}return;}
if(typeof Chart==="undefined"||typeof ChartDataLabels==="undefined"){if(attempts++<50){setTimeout(%s,300);}return;}
if(ctx.getAttribute("data-rr-init")){return;}
ctx.setAttribute("data-rr-init","1");
Chart.register(ChartDataLabels);
var _m=window.innerWidth<768;
%s
}
if(document.readyState==="complete"){%s();}
else{window.addEventListener("load",%s);}
setTimeout(%s,1000);
})();
</script>',
esc_attr( $canvas_id ),
$style,
esc_attr( $canvas_id ),
esc_attr( $canvas_id ),
esc_attr( $mobile_height ),
$fn,
esc_attr( $canvas_id ),
$fn,
$fn,
$js,
$fn,
$fn,
$fn
);
}
private function bar_datalabels() {
return "datalabels:{color:'#fff',font:{weight:'bold',size:_m?10:12},display:function(c){return c.dataset.data[c.dataIndex]>0;},anchor:'center',align:'center'}";
}
private function hbar_datalabels() {
return "datalabels:{color:function(c){var v=c.dataset.data[c.dataIndex];var max=Math.max.apply(null,c.dataset.data);return v<(max*0.15)?'#333':'#fff';},font:{weight:'bold',size:_m?9:11},display:function(c){return c.dataset.data[c.dataIndex]>0;},anchor:function(c){var v=c.dataset.data[c.dataIndex];var max=Math.max.apply(null,c.dataset.data);return v<(max*0.15)?'end':'center';},align:function(c){var v=c.dataset.data[c.dataIndex];var max=Math.max.apply(null,c.dataset.data);return v<(max*0.15)?'right':'center';}}";
}
private function line_datalabels() {
return "datalabels:{color:'#333',font:{weight:'bold',size:_m?9:11},anchor:'end',align:'top',offset:_m?2:4,display:function(c){return c.dataset.data[c.dataIndex]>0;}}";
}
/**
* 1. Geöffnet vs. Geschlossen - Donut (400px hoch)
*/
public function render_status_chart( $atts ) {
$this->enqueue();
$counts = $this->data->get_status_counts();
$id = $this->get_id();
$labels = wp_json_encode( array_map( 'ucfirst', array_keys( $counts ) ) );
$values = wp_json_encode( array_values( $counts ) );
$bg = wp_json_encode( [
$this->colors['geoeffnet'],
$this->colors['geschlossen'],
$this->colors['neuer_betreiber'],
] );
$js = "new Chart(ctx,{type:'doughnut',data:{labels:{$labels},datasets:[{data:{$values},backgroundColor:{$bg},borderWidth:2,borderColor:'#fff'}]},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:'bottom',labels:{font:{size:_m?11:14},padding:_m?10:16}},datalabels:{color:'#fff',font:{weight:'bold',size:_m?11:14},formatter:function(v,c){var t=c.dataset.data.reduce(function(a,b){return a+b},0);return v+' ('+Math.round(v/t*100)+'%)';},display:function(c){return c.dataset.data[c.dataIndex]>0;}},tooltip:{callbacks:{label:function(c){var t=c.dataset.data.reduce(function(a,b){return a+b},0);var p=Math.round(c.raw/t*100);return c.label+': '+c.raw+' ('+p+'%)';}}}}}}); ";
return $this->wrap( $id, $js, '400px', '350px' );
}
/**
* 2. Öffnungsrate pro Staffel - Stacked Bar (500px hoch)
*/
public function render_staffel_status_chart( $atts ) {
$this->enqueue();
$staffel_data = $this->data->get_staffel_status();
$id = $this->get_id();
$labels = wp_json_encode( array_map( function( $s ) {
return 'Staffel ' . $s;
}, array_keys( $staffel_data ) ) );
$ds_open = wp_json_encode( array_column( array_values( $staffel_data ), 'geöffnet' ) );
$ds_closed = wp_json_encode( array_column( array_values( $staffel_data ), 'geschlossen' ) );
$ds_new = wp_json_encode( array_column( array_values( $staffel_data ), 'neuer Betreiber' ) );
$c_open = $this->colors['geoeffnet'];
$c_closed = $this->colors['geschlossen'];
$c_new = $this->colors['neuer_betreiber'];
$dl = $this->bar_datalabels();
$js = "new Chart(ctx,{type:'bar',data:{labels:{$labels},datasets:[{label:'Geöffnet',data:{$ds_open},backgroundColor:'{$c_open}'},{label:'Geschlossen',data:{$ds_closed},backgroundColor:'{$c_closed}'},{label:'Neuer Betreiber',data:{$ds_new},backgroundColor:'{$c_new}'}]},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:'bottom',labels:{font:{size:_m?10:14},padding:_m?8:16}},{$dl}},scales:{x:{stacked:true,ticks:{font:{size:_m?9:12},maxRotation:_m?45:0}},y:{stacked:true,beginAtZero:true,ticks:{stepSize:1,font:{size:_m?10:12}}}}}});";
return $this->wrap( $id, $js, '500px', '450px' );
}
/**
* 3. Verbesserung beim Testessen - Grouped Bar (500px hoch)
*/
public function render_testessen_verbesserung_chart( $atts ) {
$this->enqueue();
$items = $this->data->get_testessen_verbesserung();
$id = $this->get_id();
$staffeln = [];
foreach ( $items as $item ) {
$s = $item['staffel'];
if ( ! isset( $staffeln[ $s ] ) ) {
$staffeln[ $s ] = [ 'sum1' => 0, 'sum2' => 0, 'c1' => 0, 'c2' => 0 ];
}
if ( $item['test1'] > 0 ) {
$staffeln[ $s ]['sum1'] += $item['test1'];
$staffeln[ $s ]['c1']++;
}
if ( $item['test2'] > 0 ) {
$staffeln[ $s ]['sum2'] += $item['test2'];
$staffeln[ $s ]['c2']++;
}
}
ksort( $staffeln, SORT_NUMERIC );
$labels = [];
$d1 = [];
$d2 = [];
foreach ( $staffeln as $s => $v ) {
$labels[] = 'Staffel ' . $s;
$d1[] = $v['c1'] > 0 ? round( $v['sum1'] / $v['c1'], 2 ) : 0;
$d2[] = $v['c2'] > 0 ? round( $v['sum2'] / $v['c2'], 2 ) : 0;
}
$labels_json = wp_json_encode( $labels );
$d1_json = wp_json_encode( $d1 );
$d2_json = wp_json_encode( $d2 );
$c1 = $this->colors['accent'];
$c2 = $this->colors['primary'];
$js = "new Chart(ctx,{type:'bar',data:{labels:{$labels_json},datasets:[{label:'1. Testessen',data:{$d1_json},backgroundColor:'{$c1}'},{label:'2. Testessen',data:{$d2_json},backgroundColor:'{$c2}'}]},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:'bottom',labels:{font:{size:_m?10:14},padding:_m?8:16}},datalabels:{color:'#fff',font:{weight:'bold',size:_m?9:11},anchor:'center',align:'center',display:function(c){return c.dataset.data[c.dataIndex]>0;}}},scales:{x:{ticks:{font:{size:_m?9:12},maxRotation:_m?45:0}},y:{beginAtZero:true,max:5,ticks:{font:{size:_m?10:12}},title:{display:true,text:'Bewertung (1-5)',font:{size:_m?10:12}}}}}});";
return $this->wrap( $id, $js, '500px', '400px' );
}
/**
* 4. Restaurants pro Bundesland - Horizontal Bar (dynamisch: 45px pro Region, min 400px)
*/
public function render_region_chart( $atts ) {
$this->enqueue();
$regions = $this->data->get_region_counts();
$count = count( $regions );
$h = max( 400, $count * 45 );
$id = $this->get_id();
$labels = wp_json_encode( array_keys( $regions ) );
$values = wp_json_encode( array_values( $regions ) );
$bg = wp_json_encode( array_slice( $this->primary_shades, 0, max( $count, 1 ) ) );
// Spezielle Datalabels für multi-color Balken: Helligkeit der BG-Farbe prüfen
$dl = "datalabels:{color:function(c){var bg=c.dataset.backgroundColor[c.dataIndex]||'#333';var r=parseInt(bg.substr(1,2),16),g=parseInt(bg.substr(3,2),16),b=parseInt(bg.substr(5,2),16);var lum=(0.299*r+0.587*g+0.114*b)/255;return lum>0.55?'#333':'#fff';},font:{weight:'bold',size:_m?9:11},display:function(c){return c.dataset.data[c.dataIndex]>0;},anchor:'center',align:'center'}";
$js = "new Chart(ctx,{type:'bar',data:{labels:{$labels},datasets:[{label:'Restaurants',data:{$values},backgroundColor:{$bg}}]},options:{indexAxis:'y',responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false},{$dl}},scales:{x:{beginAtZero:true,ticks:{stepSize:1,font:{size:_m?10:12}}},y:{ticks:{font:{size:_m?10:12}}}}}});";
$h_mobile = max( 400, $count * 38 );
return $this->wrap( $id, $js, $h . 'px', $h_mobile . 'px' );
}
/**
* 5. Überleben pro Bundesland - Stacked Horizontal Bar (dynamisch: 45px pro Region, min 400px)
*/
public function render_region_status_chart( $atts ) {
$this->enqueue();
$region_status = $this->data->get_region_status();
uasort( $region_status, function( $a, $b ) {
return array_sum( $b ) - array_sum( $a );
} );
$count = count( $region_status );
$h = max( 400, $count * 45 );
$id = $this->get_id();
$labels = wp_json_encode( array_keys( $region_status ) );
$ds_open = wp_json_encode( array_column( array_values( $region_status ), 'geöffnet' ) );
$ds_closed = wp_json_encode( array_column( array_values( $region_status ), 'geschlossen' ) );
$ds_new = wp_json_encode( array_column( array_values( $region_status ), 'neuer Betreiber' ) );
$c_open = $this->colors['geoeffnet'];
$c_closed = $this->colors['geschlossen'];
$c_new = $this->colors['neuer_betreiber'];
$dl = $this->hbar_datalabels();
$js = "new Chart(ctx,{type:'bar',data:{labels:{$labels},datasets:[{label:'Geöffnet',data:{$ds_open},backgroundColor:'{$c_open}'},{label:'Geschlossen',data:{$ds_closed},backgroundColor:'{$c_closed}'},{label:'Neuer Betreiber',data:{$ds_new},backgroundColor:'{$c_new}'}]},options:{indexAxis:'y',responsive:true,maintainAspectRatio:false,plugins:{legend:{position:'bottom',labels:{font:{size:_m?10:14},padding:_m?8:16}},{$dl}},scales:{x:{stacked:true,beginAtZero:true,ticks:{stepSize:1,font:{size:_m?10:12}}},y:{stacked:true,ticks:{font:{size:_m?10:12}}}}}});";
$h_mobile = max( 400, $count * 38 );
return $this->wrap( $id, $js, $h . 'px', $h_mobile . 'px' );
}
/**
* 6. Durchschnittliche Ergebnisse pro Staffel - Line Chart (450px hoch)
*/
public function render_staffel_ergebnis_chart( $atts ) {
$this->enqueue();
$avg_data = $this->data->get_staffel_ergebnis_avg();
$id = $this->get_id();
$labels = wp_json_encode( array_map( function( $s ) {
return 'Staffel ' . $s;
}, array_keys( $avg_data ) ) );
$d1 = wp_json_encode( array_column( array_values( $avg_data ), 'avg1' ) );
$d2 = wp_json_encode( array_column( array_values( $avg_data ), 'avg2' ) );
$c1 = $this->colors['accent'];
$c2 = $this->colors['primary'];
$dl = $this->line_datalabels();
$js = "new Chart(ctx,{type:'line',data:{labels:{$labels},datasets:[{label:'Ø 1. Testessen',data:{$d1},borderColor:'{$c1}',backgroundColor:'{$c1}22',tension:0.3,fill:true,pointRadius:_m?3:5},{label:'Ø 2. Testessen',data:{$d2},borderColor:'{$c2}',backgroundColor:'{$c2}22',tension:0.3,fill:true,pointRadius:_m?3:5}]},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:'bottom',labels:{font:{size:_m?10:14},padding:_m?8:16}},{$dl}},scales:{x:{ticks:{font:{size:_m?9:12},maxRotation:_m?45:0}},y:{beginAtZero:true,max:5,ticks:{font:{size:_m?10:12}},title:{display:true,text:'Ø Bewertung (1-5)',font:{size:_m?10:12}}}}}});";
return $this->wrap( $id, $js, '450px', '380px' );
}
}

382
includes/class-rr-data.php Normal file
View File

@@ -0,0 +1,382 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class RR_Data {
private $meta_table = 'of47_wp_rosins_restaurants_meta';
private $post_type = 'rosins-restaurants';
private $taxonomy = 'region';
private $cache_ttl = 3600; // 1 Stunde
private $table_format = null; // 'jet' oder 'wp'
/**
* Erkennt das Tabellenformat:
* - JetEngine custom tables haben eigene Spalten pro Feld (staffel, status, etc.)
* - WP-Style hat meta_key / meta_value Spalten
*/
private function detect_table_format() {
if ( null !== $this->table_format ) {
return $this->table_format;
}
global $wpdb;
$columns = $wpdb->get_results( "SHOW COLUMNS FROM {$this->meta_table}" );
if ( empty( $columns ) ) {
$this->table_format = 'wp';
return $this->table_format;
}
$col_names = array_map( function( $c ) { return $c->Field; }, $columns );
// JetEngine custom tables haben die Feldnamen direkt als Spalten
if ( in_array( 'status', $col_names, true ) ) {
$this->table_format = 'jet';
} else {
$this->table_format = 'wp';
}
return $this->table_format;
}
/**
* Build a lookup: post_id => [ field_name => value ]
*/
private function build_meta_map() {
$cached = get_transient( 'rr_meta_map' );
if ( false !== $cached ) {
return $cached;
}
global $wpdb;
$posts_table = $wpdb->posts;
$format = $this->detect_table_format();
$map = [];
if ( 'jet' === $format ) {
// JetEngine: Spalten sind direkt die Feldnamen
$results = $wpdb->get_results( $wpdb->prepare(
"SELECT p.ID as post_id, p.post_title, m.*
FROM {$posts_table} p
INNER JOIN {$this->meta_table} m ON p.ID = m.object_ID
WHERE p.post_type = %s AND p.post_status = 'publish'",
$this->post_type
) );
foreach ( $results as $row ) {
$row_arr = (array) $row;
$pid = $row_arr['post_id'];
unset( $row_arr['post_id'], $row_arr['post_title'], $row_arr['meta_ID'], $row_arr['object_ID'] );
$map[ $pid ] = $row_arr;
$map[ $pid ]['_title'] = $row->post_title;
}
} else {
// WP-Style: meta_key / meta_value
$results = $wpdb->get_results( $wpdb->prepare(
"SELECT p.ID as post_id, p.post_title, m.meta_key, m.meta_value
FROM {$posts_table} p
INNER JOIN {$this->meta_table} m ON p.ID = m.post_id
WHERE p.post_type = %s AND p.post_status = 'publish'",
$this->post_type
) );
foreach ( $results as $row ) {
$map[ $row->post_id ][ $row->meta_key ] = $row->meta_value;
$map[ $row->post_id ]['_title'] = $row->post_title;
}
}
set_transient( 'rr_meta_map', $map, $this->cache_ttl );
return $map;
}
/**
* Debug: Rohdaten für Diagnose
*/
public function get_debug_info() {
global $wpdb;
$info = [];
// Tabellenformat
$info['table_format'] = $this->detect_table_format();
// Spalten der Meta-Tabelle
$columns = $wpdb->get_results( "SHOW COLUMNS FROM {$this->meta_table}" );
$info['columns'] = $columns ? array_map( function( $c ) { return $c->Field; }, $columns ) : 'TABELLE NICHT GEFUNDEN';
// Anzahl Zeilen
$info['meta_row_count'] = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$this->meta_table}" );
// Anzahl publizierte Posts
$info['published_posts'] = (int) $wpdb->get_var( $wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = %s AND post_status = 'publish'",
$this->post_type
) );
// Beispielzeile
$info['sample_row'] = $wpdb->get_row( "SELECT * FROM {$this->meta_table} LIMIT 1", ARRAY_A );
// Meta-Map Ergebnis
// Transients löschen für frische Daten
$this->clear_cache();
$map = $this->build_meta_map();
$info['meta_map_count'] = count( $map );
$info['meta_map_sample'] = array_slice( $map, 0, 2, true );
// Status-Werte die gefunden werden
$info['status_counts'] = $this->get_status_counts();
return $info;
}
/**
* Cache löschen
*/
public function clear_cache() {
delete_transient( 'rr_meta_map' );
delete_transient( 'rr_status_counts' );
delete_transient( 'rr_staffel_status' );
delete_transient( 'rr_testessen_verb' );
delete_transient( 'rr_region_counts' );
delete_transient( 'rr_region_status' );
delete_transient( 'rr_staffel_avg' );
delete_transient( 'rr_all_meta' );
}
/**
* Normalisiert den Status-Wert für Vergleiche
*/
private function normalize_status( $raw ) {
$s = mb_strtolower( trim( $raw ) );
// Verschiedene mögliche Schreibweisen abfangen
if ( in_array( $s, [ 'geöffnet', 'geoeffnet', 'offen', 'geoffnet' ], true ) ) {
return 'geöffnet';
}
if ( in_array( $s, [ 'geschlossen', 'zu', 'closed' ], true ) ) {
return 'geschlossen';
}
if ( false !== strpos( $s, 'neuer' ) || false !== strpos( $s, 'betreiber' ) || false !== strpos( $s, 'new' ) ) {
return 'neuer Betreiber';
}
return $s;
}
/**
* 1. Status-Verteilung
*/
public function get_status_counts() {
$cached = get_transient( 'rr_status_counts' );
if ( false !== $cached ) {
return $cached;
}
$map = $this->build_meta_map();
$counts = [
'geöffnet' => 0,
'geschlossen' => 0,
'neuer Betreiber' => 0,
];
foreach ( $map as $meta ) {
$status = $this->normalize_status( $meta['status'] ?? '' );
if ( isset( $counts[ $status ] ) ) {
$counts[ $status ]++;
}
}
set_transient( 'rr_status_counts', $counts, $this->cache_ttl );
return $counts;
}
/**
* 2. Status pro Staffel
*/
public function get_staffel_status() {
$cached = get_transient( 'rr_staffel_status' );
if ( false !== $cached ) {
return $cached;
}
$map = $this->build_meta_map();
$result = [];
foreach ( $map as $meta ) {
$staffel = trim( $meta['staffel'] ?? '' );
$status = $this->normalize_status( $meta['status'] ?? '' );
if ( '' === $staffel || '' === $status ) {
continue;
}
if ( ! isset( $result[ $staffel ] ) ) {
$result[ $staffel ] = [
'geöffnet' => 0,
'geschlossen' => 0,
'neuer Betreiber' => 0,
];
}
if ( isset( $result[ $staffel ][ $status ] ) ) {
$result[ $staffel ][ $status ]++;
}
}
ksort( $result, SORT_NUMERIC );
set_transient( 'rr_staffel_status', $result, $this->cache_ttl );
return $result;
}
/**
* 3. Testessen-Verbesserung
*/
public function get_testessen_verbesserung() {
$cached = get_transient( 'rr_testessen_verb' );
if ( false !== $cached ) {
return $cached;
}
$map = $this->build_meta_map();
$result = [];
foreach ( $map as $post_id => $meta ) {
$e1 = floatval( $meta['ergebnis_1_testessen_normalisiert'] ?? 0 );
$e2 = floatval( $meta['ergebnis_2_testessen_normalisiert'] ?? 0 );
if ( $e1 <= 0 && $e2 <= 0 ) {
continue;
}
$staffel = trim( $meta['staffel'] ?? '?' );
$result[] = [
'post_id' => $post_id,
'staffel' => $staffel,
'test1' => $e1,
'test2' => $e2,
];
}
set_transient( 'rr_testessen_verb', $result, $this->cache_ttl );
return $result;
}
/**
* 4. Restaurants pro Region
*/
public function get_region_counts() {
$cached = get_transient( 'rr_region_counts' );
if ( false !== $cached ) {
return $cached;
}
$terms = get_terms( [
'taxonomy' => $this->taxonomy,
'hide_empty' => true,
] );
if ( is_wp_error( $terms ) ) {
return [];
}
$result = [];
foreach ( $terms as $term ) {
$result[ $term->name ] = (int) $term->count;
}
arsort( $result );
set_transient( 'rr_region_counts', $result, $this->cache_ttl );
return $result;
}
/**
* 5. Status pro Region
*/
public function get_region_status() {
$cached = get_transient( 'rr_region_status' );
if ( false !== $cached ) {
return $cached;
}
$terms = get_terms( [
'taxonomy' => $this->taxonomy,
'hide_empty' => true,
] );
if ( is_wp_error( $terms ) ) {
return [];
}
$meta_map = $this->build_meta_map();
$result = [];
foreach ( $terms as $term ) {
$post_ids = get_objects_in_term( $term->term_id, $this->taxonomy );
if ( is_wp_error( $post_ids ) ) {
continue;
}
$counts = [
'geöffnet' => 0,
'geschlossen' => 0,
'neuer Betreiber' => 0,
];
foreach ( $post_ids as $pid ) {
$status = $this->normalize_status( $meta_map[ $pid ]['status'] ?? '' );
if ( isset( $counts[ $status ] ) ) {
$counts[ $status ]++;
}
}
$result[ $term->name ] = $counts;
}
set_transient( 'rr_region_status', $result, $this->cache_ttl );
return $result;
}
/**
* 6. Durchschnittliche Testessen-Ergebnisse pro Staffel
*/
public function get_staffel_ergebnis_avg() {
$cached = get_transient( 'rr_staffel_avg' );
if ( false !== $cached ) {
return $cached;
}
$map = $this->build_meta_map();
$sums = [];
foreach ( $map as $meta ) {
$staffel = trim( $meta['staffel'] ?? '' );
if ( '' === $staffel ) {
continue;
}
$e1 = floatval( $meta['ergebnis_1_testessen_normalisiert'] ?? 0 );
$e2 = floatval( $meta['ergebnis_2_testessen_normalisiert'] ?? 0 );
if ( ! isset( $sums[ $staffel ] ) ) {
$sums[ $staffel ] = [ 'sum1' => 0, 'sum2' => 0, 'count1' => 0, 'count2' => 0 ];
}
if ( $e1 > 0 ) {
$sums[ $staffel ]['sum1'] += $e1;
$sums[ $staffel ]['count1']++;
}
if ( $e2 > 0 ) {
$sums[ $staffel ]['sum2'] += $e2;
$sums[ $staffel ]['count2']++;
}
}
$result = [];
ksort( $sums, SORT_NUMERIC );
foreach ( $sums as $staffel => $s ) {
$result[ $staffel ] = [
'avg1' => $s['count1'] > 0 ? round( $s['sum1'] / $s['count1'], 2 ) : 0,
'avg2' => $s['count2'] > 0 ? round( $s['sum2'] / $s['count2'], 2 ) : 0,
];
}
set_transient( 'rr_staffel_avg', $result, $this->cache_ttl );
return $result;
}
}

311
rr-auswertung.php Normal file
View File

@@ -0,0 +1,311 @@
<?php
/**
* Plugin Name: RR Auswertung
* Description: Diagramme und Statistiken für Rosins Restaurants
* Version: 1.0.0
* Author: Netz-Orth
* Text Domain: rr-auswertung
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
define( 'RR_AUSWERTUNG_PATH', plugin_dir_path( __FILE__ ) );
define( 'RR_AUSWERTUNG_URL', plugin_dir_url( __FILE__ ) );
require_once RR_AUSWERTUNG_PATH . 'includes/class-rr-data.php';
require_once RR_AUSWERTUNG_PATH . 'includes/class-rr-charts.php';
final class RR_Auswertung {
private static $instance = null;
private $data;
private $charts;
public static function instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
$this->data = new RR_Data();
$this->charts = new RR_Charts( $this->data );
add_action( 'wp_enqueue_scripts', [ $this, 'register_assets' ] );
$this->register_shortcodes();
}
public function register_assets() {
wp_register_script(
'chartjs',
RR_AUSWERTUNG_URL . 'assets/js/chart.min.js',
[],
'4.4.7',
false
);
wp_register_script(
'chartjs-datalabels',
RR_AUSWERTUNG_URL . 'assets/js/chartjs-datalabels.min.js',
[ 'chartjs' ],
'2.2.0',
false
);
wp_register_style(
'rr-charts',
RR_AUSWERTUNG_URL . 'assets/css/rr-charts.css',
[],
'1.0.0'
);
// Perfmatters / WP Rocket / Autoptimize: chart.min.js von Delay/Defer ausschließen
add_filter( 'perfmatters_delay_js_exclusions', function( $exclusions ) {
$exclusions[] = 'chart.min.js';
$exclusions[] = 'chartjs-datalabels.min.js';
return $exclusions;
} );
add_filter( 'perfmatters_defer_js_exclusions', function( $exclusions ) {
$exclusions[] = 'chart.min.js';
$exclusions[] = 'chartjs-datalabels.min.js';
return $exclusions;
} );
}
private function register_shortcodes() {
$shortcodes = [
'rr_status_chart' => 'render_status_chart',
'rr_staffel_status_chart' => 'render_staffel_status_chart',
'rr_testessen_verbesserung_chart' => 'render_testessen_verbesserung_chart',
'rr_region_chart' => 'render_region_chart',
'rr_region_status_chart' => 'render_region_status_chart',
'rr_staffel_ergebnis_chart' => 'render_staffel_ergebnis_chart',
];
foreach ( $shortcodes as $tag => $method ) {
add_shortcode( $tag, [ $this->charts, $method ] );
}
// Text-Shortcodes mit dynamischen Zahlen
add_shortcode( 'rr_text_status', [ $this, 'render_text_status' ] );
add_shortcode( 'rr_text_staffel', [ $this, 'render_text_staffel' ] );
add_shortcode( 'rr_text_testessen', [ $this, 'render_text_testessen' ] );
add_shortcode( 'rr_text_region', [ $this, 'render_text_region' ] );
add_shortcode( 'rr_text_region_status', [ $this, 'render_text_region_status' ] );
add_shortcode( 'rr_text_staffel_ergebnis', [ $this, 'render_text_staffel_ergebnis' ] );
add_shortcode( 'rr_text_intro', [ $this, 'render_text_intro' ] );
// Debug-Shortcode: zeigt Rohdaten + Chart.js Status
add_shortcode( 'rr_debug', [ $this, 'render_debug' ] );
}
public function render_text_intro( $atts ) {
$counts = $this->data->get_status_counts();
$total = array_sum( $counts );
$staffel_data = $this->data->get_staffel_status();
$staffeln = count( $staffel_data );
$rate = $total > 0 ? round( $counts['geöffnet'] / $total * 100 ) : 0;
return sprintf(
'<p><strong>„Rosins Restaurants"</strong> gehört zu den bekanntesten deutschen TV-Formaten, wenn es um die Gastronomie geht. Seit der ersten Staffel hat Sternekoch <strong>Frank Rosin</strong> in <strong>%d Staffeln</strong> insgesamt <strong>%d Restaurants</strong> in ganz Deutschland besucht und den Betreibern unter die Arme gegriffen. Doch wie nachhaltig ist seine Hilfe wirklich? Aktuell sind noch <strong>%d Restaurants (%d%%)</strong> geöffnet. Auf dieser Seite findest du alle Zahlen, Daten und Fakten zur Sendung die Statistiken werden regelmäßig aktualisiert und spiegeln den aktuellen Stand der besuchten Betriebe wider.</p>',
$staffeln,
$total,
$counts['geöffnet'],
$rate
);
}
public function render_text_status( $atts ) {
$counts = $this->data->get_status_counts();
$total = array_sum( $counts );
$rate_open = $total > 0 ? round( $counts['geöffnet'] / $total * 100 ) : 0;
$rate_closed = $total > 0 ? round( $counts['geschlossen'] / $total * 100 ) : 0;
$rate_new = $total > 0 ? round( $counts['neuer Betreiber'] / $total * 100 ) : 0;
return sprintf(
'<p>Eine der häufigsten Fragen rund um die Sendung lautet: Wie viele der von Frank Rosin besuchten Restaurants existieren heute noch? Von insgesamt <strong>%d Restaurants</strong> sind aktuell noch <strong>%d geöffnet</strong> das entspricht einer Überlebensrate von <strong>%d%%</strong>. Dem gegenüber stehen <strong>%d Betriebe (%d%%)</strong>, die dauerhaft schließen mussten. Weitere <strong>%d Restaurants (%d%%)</strong> werden heute unter neuen Betreibern weitergeführt das ursprüngliche Konzept oder der Besitzer hat also gewechselt, der Standort ist aber weiterhin in Betrieb.</p>',
$total,
$counts['geöffnet'],
$rate_open,
$counts['geschlossen'],
$rate_closed,
$counts['neuer Betreiber'],
$rate_new
);
}
public function render_text_staffel( $atts ) {
$staffel_data = $this->data->get_staffel_status();
$best_staffel = '';
$best_rate = 0;
$worst_staffel = '';
$worst_rate = 100;
foreach ( $staffel_data as $s => $data ) {
$sum = array_sum( $data );
if ( $sum > 0 ) {
$rate = $data['geöffnet'] / $sum * 100;
if ( $rate > $best_rate ) {
$best_rate = $rate;
$best_staffel = $s;
}
if ( $rate < $worst_rate ) {
$worst_rate = $rate;
$worst_staffel = $s;
}
}
}
return sprintf(
'<p>Nicht jede Staffel von Rosins Restaurants war gleich erfolgreich. Über <strong>%d Staffeln</strong> hinweg zeigen sich deutliche Unterschiede bei der langfristigen Überlebensrate der besuchten Betriebe. Die erfolgreichste Staffel ist <strong>Staffel %s</strong> hier sind noch <strong>%d%%</strong> der Restaurants geöffnet. Am schlechtesten schneidet <strong>Staffel %s</strong> ab, wo nur noch <strong>%d%%</strong> der Betriebe existieren. Das folgende Diagramm schlüsselt die Verteilung von geöffneten, geschlossenen und unter neuer Führung stehenden Restaurants pro Staffel auf.</p>',
count( $staffel_data ),
$best_staffel,
round( $best_rate ),
$worst_staffel,
round( $worst_rate )
);
}
public function render_text_testessen( $atts ) {
$avg_data = $this->data->get_staffel_ergebnis_avg();
$total_avg1 = 0;
$total_avg2 = 0;
$count = 0;
foreach ( $avg_data as $s => $v ) {
if ( $v['avg1'] > 0 && $v['avg2'] > 0 ) {
$total_avg1 += $v['avg1'];
$total_avg2 += $v['avg2'];
$count++;
}
}
$avg1 = $count > 0 ? round( $total_avg1 / $count, 1 ) : 0;
$avg2 = $count > 0 ? round( $total_avg2 / $count, 1 ) : 0;
$diff = round( $avg2 - $avg1, 1 );
$pct = $avg1 > 0 ? round( $diff / $avg1 * 100 ) : 0;
return sprintf(
'<p>Ein zentrales Element jeder Folge ist das Testessen: Ein unabhängiges Team bewertet die Qualität der Restaurants auf einer normalisierten Skala von <strong>1 bis 5</strong>. Das erste Testessen findet vor Frank Rosins Eingreifen statt, das zweite nach der Umgestaltung. Im Durchschnitt über alle Staffeln verbessern sich die Restaurants von <strong>%.1f</strong> auf <strong>%.1f Punkte</strong> das ist eine Steigerung um <strong>%.1f Punkte (+%d%%)</strong>. Doch wie konstant ist diese Verbesserung über die Jahre? Das folgende Diagramm zeigt den Vorher-Nachher-Vergleich der durchschnittlichen Testessen-Ergebnisse, aufgeschlüsselt nach Staffel.</p>',
$avg1,
$avg2,
$diff,
$pct
);
}
public function render_text_region( $atts ) {
$regions = $this->data->get_region_counts();
$top_region = '';
$top_count = 0;
$second_region = '';
$second_count = 0;
foreach ( $regions as $name => $c ) {
if ( $c > $top_count ) {
$second_count = $top_count;
$second_region = $top_region;
$top_count = $c;
$top_region = $name;
} elseif ( $c > $second_count ) {
$second_count = $c;
$second_region = $name;
}
}
return sprintf(
'<p>Die von Frank Rosin besuchten Restaurants verteilen sich auf <strong>%d Bundesländer und Regionen</strong> darunter auch vereinzelte Einsätze im Ausland. Die regionale Verteilung ist dabei keineswegs gleichmäßig: Die meisten Hilferufe kamen aus <strong>%s</strong> mit <strong>%d Restaurants</strong>, gefolgt von <strong>%s</strong> mit <strong>%d Betrieben</strong>. Diese Verteilung spiegelt unter anderem die Dichte der Gastronomie-Szene und die wirtschaftliche Situation in den jeweiligen Regionen wider. Das horizontale Balkendiagramm zeigt die genaue Aufschlüsselung nach Bundesland und Region.</p>',
count( $regions ),
$top_region,
$top_count,
$second_region,
$second_count
);
}
public function render_text_region_status( $atts ) {
$region_status = $this->data->get_region_status();
$best_region = '';
$best_rate = 0;
$worst_region = '';
$worst_rate = 100;
foreach ( $region_status as $name => $data ) {
$sum = array_sum( $data );
if ( $sum >= 3 ) {
$rate = $data['geöffnet'] / $sum * 100;
if ( $rate > $best_rate ) {
$best_rate = $rate;
$best_region = $name;
}
if ( $rate < $worst_rate ) {
$worst_rate = $rate;
$worst_region = $name;
}
}
}
return sprintf(
'<p>Gibt es regionale Unterschiede beim langfristigen Erfolg der Restaurants? Die Daten zeigen: Ja. Betrachtet man nur Bundesländer und Regionen mit mindestens drei besuchten Restaurants, hat <strong>%s</strong> mit <strong>%d%%</strong> die höchste Überlebensrate. Am anderen Ende steht <strong>%s</strong> mit nur <strong>%d%%</strong> noch geöffneten Betrieben. Diese Unterschiede können auf verschiedene Faktoren zurückzuführen sein von der lokalen Wirtschaftslage über Mietpreise bis hin zur Wettbewerbssituation vor Ort.</p>',
$best_region,
round( $best_rate ),
$worst_region,
round( $worst_rate )
);
}
public function render_text_staffel_ergebnis( $atts ) {
$avg_data = $this->data->get_staffel_ergebnis_avg();
$keys = array_keys( $avg_data );
$first = reset( $keys );
$last = end( $keys );
$first_avg2 = $avg_data[ $first ]['avg2'] ?? 0;
$last_avg2 = $avg_data[ $last ]['avg2'] ?? 0;
$trend = $last_avg2 > $first_avg2 ? 'verbessert' : ( $last_avg2 < $first_avg2 ? 'verschlechtert' : 'kaum verändert' );
// Beste Staffel nach 2. Testessen finden
$best_s = '';
$best_avg2 = 0;
foreach ( $avg_data as $s => $v ) {
if ( $v['avg2'] > $best_avg2 ) {
$best_avg2 = $v['avg2'];
$best_s = $s;
}
}
return sprintf(
'<p>Wie haben sich die Testessen-Bewertungen im Laufe der Sendung entwickelt? Von Staffel %s bis Staffel %s hat sich das durchschnittliche Ergebnis nach Rosins Einsatz insgesamt <strong>%s</strong> von <strong>%.1f</strong> auf <strong>%.1f Punkte</strong>. Die stärkste Verbesserung nach dem Coaching erreichten die Restaurants in <strong>Staffel %s</strong> mit einem Durchschnitt von <strong>%.1f Punkten</strong> beim zweiten Testessen. Die beiden Linien im Diagramm zeigen den Trend der normalisierten Ergebnisse vor und nach Frank Rosins Einsatz über alle Staffeln hinweg.</p>',
$first,
$last,
$trend,
$first_avg2,
$last_avg2,
$best_s,
$best_avg2
);
}
public function render_debug( $atts ) {
if ( ! current_user_can( 'manage_options' ) ) {
return '<p>Debug nur für Admins sichtbar.</p>';
}
// Cache leeren für frische Daten
$this->data->clear_cache();
$info = $this->data->get_debug_info();
$out = '<div style="background:#f0f0f0;padding:20px;font-family:monospace;font-size:13px;overflow:auto;max-height:800px;">';
$out .= '<h3>RR Auswertung Debug</h3>';
$out .= '<pre>' . esc_html( print_r( $info, true ) ) . '</pre>';
$out .= '<h4>Chart.js Test</h4>';
$out .= '<p id="rr-debug-chartjs">Prüfe Chart.js...</p>';
$out .= '<script>document.addEventListener("DOMContentLoaded",function(){var el=document.getElementById("rr-debug-chartjs");if(typeof Chart!=="undefined"){el.innerHTML="Chart.js geladen: v"+Chart.version;}else{el.innerHTML="FEHLER: Chart.js NICHT geladen!";el.style.color="red";}});</script>';
$out .= '</div>';
return $out;
}
}
add_action( 'plugins_loaded', [ 'RR_Auswertung', 'instance' ] );