STA-003
Horizontal bar chart in SVG
For ranked lists — top pages, top referrers, top earners. Horizontal because labels are words, and words do not fit under a vertical bar.
Ranked data is the most common thing on an admin dashboard and the most commonly drawn wrong. Vertical bars force the labels to rotate, which makes them hard to read and eats vertical space.
Horizontal bars give each label a full line at normal reading angle. The bar length is a straight proportion of the maximum, so the widest bar fills the plot and everything else is relative to it.
Same as the line chart: server-rendered, no library, native SVG tooltips through a title element.
function chart_hbars(array $rows, int $w = 720, int $labelW = 220, string $color = '#2E6FD6'): string
{
$rowH = 24;
$h = max(40, count($rows) * $rowH + 10);
$barMax = $w - $labelW - 60; // leave room for the value text
$max = 1;
foreach ($rows as $r) {
$max = max($max, (int) $r['value']);
}
$svg = '<svg viewBox="0 0 ' . $w . ' ' . $h . '" role="img" xmlns="http://www.w3.org/2000/svg">';
$y = 6;
foreach ($rows as $r) {
$label = (string) $r['label'];
$value = (int) $r['value'];
$barW = max(1, (int) round($barMax * $value / $max));
$shown = strlen($label) > 38 ? rtrim(substr($label, 0, 38)) . '…' : $label;
$svg .= '<text x="0" y="' . ($y + 13) . '" font-size="11" fill="#25313F">'
. htmlspecialchars($shown) . '</text>';
$svg .= '<rect x="' . $labelW . '" y="' . $y . '" width="' . $barW . '" height="16" rx="2" fill="' . $color . '">'
. '<title>' . htmlspecialchars($label . ': ' . $value) . '</title></rect>';
$svg .= '<text x="' . ($labelW + $barW + 6) . '" y="' . ($y + 13) . '" font-size="11" fill="#6B7B8F">'
. $value . '</text>';
$y += $rowH;
}
return $svg . '</svg>';
}
Using it
Cap the list at ten to fifteen rows. A ranked chart with fifty bars is a table wearing a costume — use a table.
Put the full label in the title element even when the visible text is truncated, so hovering recovers it.
What bites people
Guard the maximum against zero before dividing, or an empty dataset produces NaN widths and an invisible chart.
Truncating by byte length breaks multibyte labels mid-character. Use the mbstring functions where they are available, with a byte fallback.