htmlspecialchars((string) $s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); // Barra proporcional — comparar números numa coluna é mais lento do que // comparar comprimentos, e a pergunta aqui é sempre "qual é o maior". $bar = function (int $n, int $max) use ($h): string { $pct = $max > 0 ? max(2, (int) round($n / $max * 100)) : 0; return '
' . '
'; }; $table = function (string $title, array $rows, callable $label) use ($h, $bar): string { if (!$rows) { return '
' . '
' . $h($title) . '
' . '

Sem dados no período.

'; } $max = max(array_map(fn($r) => (int) $r['visits'], $rows)); $out = '
' . '
' . $h($title) . '
' . ''; foreach ($rows as $r) { $out .= '' . '' . '' . '' . ''; } return $out . '
' . $label($r) . '' . (int) $r['visits'] . '' . $bar((int) $r['visits'], $max) . '
'; }; ?>

Origem das visitas

De onde vêm as pessoas, com que dispositivo e de que país.

'7 dias', 30 => '30 dias', 90 => '90 dias', 365 => '1 ano'] as $d => $lbl): ?>
Sem índice de países. A coluna geográfica fica vazia até correr shared/platform-tools/bin/geo-build-index.php. O resto funciona na mesma.
Visitas
Conversões
Taxa
0 ? number_format($totalConv / $totalVisits * 100, 1) . '%' : '—' ?>
Por origem

Ainda não há visitas registadas no período.

Origem Visitas Visitantes Conversões Taxa Última
0 ? number_format($r['conversions'] / $r['visits'] * 100, 1) . '%' : '—' ?>

A origem é a de primeiro toque: quem chega por um QR code, sai e volta pelo Google continua creditado ao QR code. Bots excluídos.

Conversões por formulário
'' . $h($r['label']) . '') ?>
$h($r['label'])) ?>
$h($r['label'])) ?>
$h($r['label'])) ?>
'' . $h($r['label']) . '') ?>

Nenhum endereço IP é guardado. O ip_hash serve só para aproximar visitantes únicos e é salgado com um segredo do servidor, por isso não se reverte.