'; $this->render_score_tile( $current_score, $prev_score, $cited_count, $total_count, $health ); $this->render_cost_tile( $mtd_cost, $cap, $pct, $truncated ); $this->render_lastrun_tile( $last_run_at, $row_count ); echo ''; } /** * Render the Chart.js trendline with config-change vertical markers. * * The chart lives in a fixed-height .prv-chartbox (300px) and is created * inside a typeof-Chart guard. If Chart.js is unavailable the guard adds * .prv-noscript, hiding the canvas and showing an in-box fallback note * without reflow or throw. * * Config-change markers are detected from consecutive config_version values * in the trendline. An inline configMarker Chart.js plugin draws a vertical * dashed orange line + pin circle at each boundary index. * * @param array $trendline Trendline data points. * * @return void */ public function render_trendline( array $trendline ): void { echo '
'; echo '

' . esc_html__( 'Visibility Score — Trend', 'pr-vision' ) . '

'; if ( empty( $trendline ) ) { echo '

' . esc_html__( 'No probe runs recorded yet. Click "Run now" to collect the first data point.', 'pr-vision' ) . '

'; return; } $labels = array(); $scores = array(); $marker_indices = array(); $prev_version = null; foreach ( $trendline as $idx => $point ) { $labels[] = gmdate( 'M j', (int) strtotime( (string) $point['captured_at'] ) ); $scores[] = round( (float) $point['score'] * 100, 2 ); $cv = isset( $point['config_version'] ) ? (int) $point['config_version'] : null; if ( null !== $prev_version && null !== $cv && $cv !== $prev_version ) { $marker_indices[] = $idx; } $prev_version = $cv; } $labels_json = wp_json_encode( $labels ); $scores_json = wp_json_encode( $scores ); $marker_indices_json = wp_json_encode( $marker_indices ); $has_markers = ! empty( $marker_indices ); echo '
'; echo ''; echo '
' . esc_html__( 'Trend chart needs Chart.js — the data still reads in the standings table below.', 'pr-vision' ) . '
'; echo '
'; echo '
'; echo '' . esc_html__( 'Visibility score (%)', 'pr-vision' ) . ''; echo '' . esc_html__( 'First-citation goal', 'pr-vision' ) . ''; if ( $has_markers ) { echo '' . esc_html__( 'Config changed', 'pr-vision' ) . ''; } echo '
'; if ( $has_markers ) { echo '
'; echo '' . esc_html__( 'Scored config changed', 'pr-vision' ) . ' — '; echo esc_html__( 'segments either side of the orange markers are separate baselines; the line does not represent one continuous trend across breaks.', 'pr-vision' ); echo '
'; } // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped echo ''; // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped echo ''; } /** * Emit the configMarker inline Chart.js plugin (no external CDN). * * Draws a vertical dashed orange line + pin circle at each marker index. * Must be called inside a