$label, 'value' => $value, 'prev' => $prev_text, 'change' => $change, 'dir' => self::change_dir( $change, $cur, $prev ), 'color' => $color, ); }; if ( isset( $d['search']['cur'] ) ) { $c = $d['search']['cur']; $p = $d['search']['prev']; $tiles[] = $tile( __( 'Google clicks', 'turtlereport' ), $c['clicks'], $p['clicks'], Format::number( $c['clicks'] ), Format::number( $p['clicks'] ), Format::delta( $c['clicks'], $p['clicks'] ), self::CLICKS ); $tiles[] = $tile( __( 'Times seen in Google', 'turtlereport' ), $c['impressions'], $p['impressions'], Format::number( $c['impressions'] ), Format::number( $p['impressions'] ), Format::delta( $c['impressions'], $p['impressions'] ), self::AI ); } if ( isset( $d['conversions']['cur'] ) && ( ! isset( $d['conversions']['status'] ) || 'no_events' !== $d['conversions']['status'] ) ) { $cv = $d['conversions']; $tiles[] = $tile( $cv['label'], $cv['cur'], $cv['prev'], Format::number( $cv['cur'] ), Format::number( $cv['prev'] ), Format::delta( $cv['cur'], $cv['prev'], 'abs' ), self::LEAD ); } if ( isset( $d['business']['cur'] ) ) { $c = $d['business']['cur']; $p = $d['business']['prev']; $tiles[] = $tile( __( 'Profile calls', 'turtlereport' ), $c['calls'], $p['calls'], Format::number( $c['calls'] ), Format::number( $p['calls'] ), Format::delta( $c['calls'], $p['calls'], 'abs' ), self::CALL ); } if ( isset( $d['search']['cur'] ) ) { $c = $d['search']['cur']; $p = $d['search']['prev']; $both = $c['position'] && $p['position']; $tiles[] = $tile( __( 'Average position', 'turtlereport' ), $both ? $c['position'] : 0, $both ? $p['position'] : 0, $c['position'] ? number_format_i18n( $c['position'], 1 ) : '–', $p['position'] ? number_format_i18n( $p['position'], 1 ) : '', $both ? Format::delta( $c['position'], $p['position'], 'pos' ) : array( 'text' => '', 'color' => Format::FLAT, ), self::POS ); $tiles[] = $tile( __( 'Click-through rate', 'turtlereport' ), $c['ctr'], $p['ctr'], Format::metric( 'pct', $c['ctr'] ), Format::metric( 'pct', $p['ctr'] ), Format::delta( $c['ctr'], $p['ctr'], 'pts' ), $brand ); } if ( count( $tiles ) < 6 && isset( $d['traffic']['metrics'][0] ) ) { foreach ( $d['traffic']['metrics'] as $m ) { if ( count( $tiles ) >= 6 ) { break; } $tiles[] = $tile( $m['label'], $m['cur'], $m['prev'], Format::metric( $m['format'], $m['cur'] ), Format::metric( $m['format'], $m['prev'] ), Format::delta( $m['cur'], $m['prev'], 'pct' === $m['format'] ? 'pts' : 'rel', ! empty( $m['lower'] ) ), self::AI ); } } if ( count( $tiles ) < 6 && isset( $d['ai']['cur'] ) ) { $tiles[] = $tile( __( 'Visits from AI assistants', 'turtlereport' ), $d['ai']['cur'], $d['ai']['prev'], Format::number( $d['ai']['cur'] ), Format::number( $d['ai']['prev'] ), Format::delta( $d['ai']['cur'], $d['ai']['prev'], 'abs' ), self::AI ); } return array_slice( $tiles, 0, 6 ); } /** * The comparison rows grouped into one card per source, each with its * colour and where the numbers come from. A breakdown of two or more * (lead types) becomes bars; a single one reads better as another cell. * * @param array[] $rows Comparison::rows(). * @param array $d Summary data. * @param Profile $site Site. * @param string $brand Brand colour. * @return array[] Items: title, hint, color, cells (rows), bars (rows). */ public static function sections( array $rows, array $d, Profile $site, $brand ) { $label = isset( $d['conversions']['label'] ) ? $d['conversions']['label'] : $site->conversion_label(); /* translators: %s: what the site calls conversions. */ $leads = sprintf( __( '%s from organic search', 'turtlereport' ), $label ); $known = array( __( 'Google search', 'turtlereport' ) => array( self::CLICKS, __( 'Search Console', 'turtlereport' ) ), $leads => array( self::LEAD, __( 'GA4 · each event counted once', 'turtlereport' ) ), __( 'Website traffic', 'turtlereport' ) => array( $brand, isset( $d['traffic']['scope'] ) && 'all' === $d['traffic']['scope'] ? __( 'GA4 · all channels', 'turtlereport' ) : __( 'GA4 · organic search', 'turtlereport' ) ), __( 'AI assistants', 'turtlereport' ) => array( self::AI, __( 'ChatGPT, Gemini, Perplexity, Copilot, Claude', 'turtlereport' ) ), __( 'Google Business Profile', 'turtlereport' ) => array( self::CALL, '' ), ); $out = array(); foreach ( $rows as $r ) { $k = $r['section']; if ( ! isset( $out[ $k ] ) ) { $out[ $k ] = array( 'title' => $k, 'color' => isset( $known[ $k ] ) ? $known[ $k ][0] : self::INK2, 'hint' => isset( $known[ $k ] ) ? $known[ $k ][1] : '', 'cells' => array(), 'bars' => array(), ); } $r['dir'] = self::row_dir( $r ); $out[ $k ][ $r['sub'] ? 'bars' : 'cells' ][] = $r; } foreach ( $out as $k => $s ) { if ( count( $s['bars'] ) < 2 || ! $s['cells'] ) { $out[ $k ]['cells'] = array_merge( $s['cells'], $s['bars'] ); $out[ $k ]['bars'] = array(); } } return array_values( $out ); } /** * Arrow direction for a comparison row. * * @param array $r Row from Comparison::rows(). * @return int 1 up, -1 down, 0 none. */ public static function row_dir( array $r ) { return self::change_dir( $r['change'], isset( $r['value'] ) ? $r['value'] : 0, isset( $r['before'] ) ? $r['before'] : 0 ); } /** * Arrow direction for a change. The arrow shows which way the number * went; the colour shows whether that's good. Average position uses * "▲"/"▼" for moving up or down the results, so those decide. * * @param array $change Format::delta() result. * @param float $cur This period. * @param float $prev Before. * @return int */ public static function change_dir( array $change, $cur, $prev ) { $text = (string) $change['text']; if ( 0 === strpos( $text, '▲' ) ) { return 1; } if ( 0 === strpos( $text, '▼' ) ) { return -1; } $d = (float) $cur - (float) $prev; return abs( $d ) < 0.0001 ? 0 : ( $d > 0 ? 1 : -1 ); } /** * Change text without its sign, for showing after an arrow: "+26%" → "26%". * * @param string $text Change text. * @return string */ public static function unsigned( $text ) { return (string) preg_replace( '/^(\+|−|-|▲ |▼ )/u', '', (string) $text ); } /** * Short name of the earlier period for "Aug 2026: 1,017". * * @param array $p Period with ps and pe. * @param bool $short Month only ("Aug"). * @return string */ public static function before_label( array $p, $short = false ) { $first = '01' === substr( $p['ps'], 8, 2 ); $last = gmdate( 'Y-m-t', strtotime( $p['ps'] . ' 12:00:00 UTC' ) ) === $p['pe']; if ( $first && $last ) { return wp_date( $short ? 'M' : 'M Y', strtotime( $p['ps'] . ' 12:00:00 UTC' ), new \DateTimeZone( 'UTC' ) ); } return __( 'Before', 'turtlereport' ); } }