SVG Generation in Ruby: A Practical Guide

SVG Generation in Ruby: A Practical Guide
SVG Generation in Ruby: A Practical Guide

March 16, 2026

Recently, I created ruby-libgd and libgd-gis for raster graphics generation in cartography. But as I worked on these tools, I realized the landscape of web graphics has shifted significantly. Today’s web demands interactive, scalable, responsive visualizations. That’s where SVG (Scalable Vector Graphics) comes in.

This article covers everything you need to know about generating SVG in Ruby: what it is, why it matters, the available tools, and how to use them with practical examples.

What is SVG?

SVG is an XML-based format for describing 2D graphics using geometric shapes and mathematical curves. Instead of storing pixels like raster formats (PNG, JPEG), SVG stores instructions: “draw a circle here,” “fill this polygon with blue,” “animate this element.”

When a browser renders SVG, it interprets these instructions at whatever resolution the device has. A map defined in SVG looks crisp on a phone, tablet, or massive monitor. It scales infinitely without quality loss.

Consider this minimal SVG:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 300">
<rect x="10" y="10" width="380" height="280" fill="#f9f9f9" stroke="#ccc"/>
<circle cx="200" cy="150" r="80" fill="blue" stroke="navy" stroke-width="2"/>
<text x="200" y="160" text-anchor="middle" fill="white">Hello SVG</text>
</svg>

Three elements: a background rectangle, a blue circle, and text. The viewBox=”0 0 400 300″ defines the coordinate system—the key to scalability. Whether you display this at 400×300 or 1200×900 pixels, the proportions remain perfect.

Why SVG Matters

Scalability: Raster maps rendered at 1200×800 look blurry at 2400×1600. SVG remains crisp at any size.

Interactivity: SVG elements are DOM nodes. You can add hover effects, click handlers, tooltips, and animations. A raster image is just pixels—no interactivity possible.

File Size: A complex vector map might be kilobytes; the same map as raster is megabytes. For web delivery, this matters.

Responsiveness: SVG adapts naturally to mobile, tablet, and desktop screens without special handling.

Animation: CSS and JavaScript control SVG properties natively. Smooth transitions, real-time updates, data-driven visualization—all possible.

Accessibility: SVG content is semantic XML. Screen readers can understand it; raster images cannot.

SVG Fundamentals

SVG provides a toolkit of elements for drawing:

Circle: <circle cx=”100″ cy=”100″ r=”50″ fill=”blue”/>

Rectangle: <rect x=”10″ y=”20″ width=”100″ height=”80″ fill=”red”/>

Polygon: <polygon points=”100,50 150,150 50,150″ fill=”green”/>

Text: <text x=”100″ y=”50″ font-size=”24″ fill=”black”>Hello</text>

Path: The most powerful element, allowing arbitrary shape drawing:

<path d="M 100 50 L 200 100 L 150 200 Z" fill="blue"/>

Path data uses commands: M (move), L (line), C (cubic curve), Q (quadratic curve), A (arc), Z (close). The above draws a triangle.

Group: <g> containers multiple elements and applies transformations:

<g transform="translate(100, 50) rotate(45)">
<circle cx="0" cy="0" r="30" fill="blue"/>
</g>

SVG is styled with inline attributes or CSS classes. You can add interactivity with event handlers:

<circle cx="100" cy="100" r="50" fill="blue"
onclick="alert('Clicked!')"
onmouseover="this.style.fill='red'"/>

The Ruby SVG Ecosystem

Five main approaches exist for generating SVG in Ruby. Each has different strengths:

Victor: The Clean Choice

Victor is purpose-built for SVG. It provides a clean, chainable DSL:

require 'victor'
svg = Victor::SVG.new(viewBox: '0 0 800 600', width: '800px', height: '600px')
svg.style(type: 'text/css') do
svg << '.interactive { cursor: pointer; } .interactive:hover { fill: red; }'
end
svg.circle(cx: 400, cy: 300, r: 100, fill: 'blue', class: 'interactive')
svg.text('Click me', x: 400, y: 310, text_anchor: 'middle', fill: 'white', font_size: 20)
puts svg.render
svg.save('map.svg')

Victor reads naturally in Ruby. Methods chain fluently. It handles SVG-specific details without boilerplate. The community is active, documentation is excellent, and it’s the recommended choice for most projects.

Pros: Clean syntax, purpose-built for SVG, well-documented, active maintenance.

Cons: Less flexible for edge cases, slightly slower than raw strings.

Nokogiri: Maximum Control

Nokogiri is a general-purpose XML builder. It gives you complete control but requires more verbose code:

require 'nokogiri'
builder = Nokogiri::XML::Builder.new do |xml|
xml.svg(xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 800 600') do
xml.defs do
xml.style(type: 'text/css') do
xml.text!('.interactive { cursor: pointer; } .interactive:hover { fill: red; }')
end
end
xml.circle(cx: 400, cy: 300, r: 100, fill: 'blue', class: 'interactive')
xml.text_('Click me', x: 400, y: 310, 'text-anchor': 'middle', fill: 'white', 'font-size': 20)
end
end
puts builder.to_xml

Nokogiri handles complex XML hierarchies and edge cases gracefully. Use it when you need maximum flexibility or are manipulating existing SVG files.

Pros: Maximum flexibility, handles complex XML, excellent for edge cases.

Cons: Verbose syntax, requires XML knowledge, not SVG-specific.

Builder: Rails Standard

Builder is lightweight and often pre-installed in Rails projects:

require 'builder'
xml = Builder::XmlMarkup.new
xml.svg(xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 800 600') do
xml.circle(cx: 400, cy: 300, r: 100, fill: 'blue')
xml.text_('Click me', x: 400, y: 310, 'text-anchor': 'middle', fill: 'white')
end
puts xml.target!

Builder is minimalist and very fast. Perfect for Rails views and performance-critical scenarios.

Pros: Zero dependencies in Rails, extremely fast, simple.

Cons: Generic XML approach, less intuitive for SVG specifically.

SVG Gem: Focused Tool

SVG is purpose-built but less actively maintained:

require 'svg'
svg = SVG.draw(viewBox: '0 0 800 600') do |canvas|
canvas.circle(cx: 400, cy: 300, r: 100, fill: 'blue')
canvas.text('Click me', x: 400, y: 310, font_size: 20)
end
puts svg.render

Clean and focused, but lacks documentation and community compared to Victor.

Pros: Purpose-built for SVG, clean API.

Cons: Less maintenance, smaller community.

String Interpolation: Minimalist

For simple SVGs, plain Ruby strings work:

def simple_map(width, height)
%{
<svg viewBox="0 0 #{width} #{height}" width="#{width}px" height="#{height}px">
<circle cx="#{width/2}" cy="#{height/2}" r="50" fill="blue"/>
</svg>
}
end
puts simple_map(800, 600)

No dependencies, fastest execution, perfect for templates.

Pros: No overhead, fastest, easiest for simple cases.

Cons: Escape special characters, hard to maintain for complex SVGs, no abstraction.

Practical Examples

Example 1: Interactive Choropleth Map

Let’s build a map that colors regions by population density:

require 'victor'
class PopulationMap
def initialize(regions)
@regions = regions # [{name:, coordinates:, population:}, ...]
@svg = Victor::SVG.new(viewBox: '0 0 1000 800')
end
def render
@svg.style(type: 'text/css') do
@svg << %{
.region { stroke: #333; stroke-width: 1; cursor: pointer; }
.region:hover { opacity: 0.7; filter: drop-shadow(0 0 5px rgba(0,0,0,0.3)); }
}
end
min_pop = @regions.map { |r| r[:population] }.min
max_pop = @regions.map { |r| r[:population] }.max
range = max_pop - min_pop
@regions.each do |region|
# Calculate color based on population
normalized = (region[:population] - min_pop) / range.to_f
color = interpolate_color(normalized)
# Convert coordinates to SVG polygon
points = region[:coordinates].map { |c| "#{c[0]},#{c[1]}" }.join(' ')
@svg.polygon(
points: points,
fill: color,
class: 'region',
'data-name': region[:name],
'data-population': region[:population],
onclick: "alert('#{region[:name]}: #{region[:population]} people')"
)
end
@svg.render
end
private
def interpolate_color(normalized)
# Blue to Red gradient
r = (0x37 + (0xe7 - 0x37) * normalized).to_i
g = (0x8A + (0x4c - 0x8A) * normalized).to_i
b = (0xDD + (0x3c - 0xDD) * normalized).to_i
"##{r.to_s(16).rjust(2, '0')}#{g.to_s(16).rjust(2, '0')}#{b.to_s(16).rjust(2, '0')}"
end
end
# Usage
regions = [
{ name: 'North', coordinates: [[0,0], [100,0], [100,100], [0,100]], population: 50000 },
{ name: 'South', coordinates: [[100,0], [200,0], [200,100], [100,100]], population: 35000 },
{ name: 'East', coordinates: [[200,0], [300,0], [300,100], [200,100]], population: 70000 }
]
map = PopulationMap.new(regions)
puts map.render

This generates an interactive map where regions are colored by population. Hover for effects, click for details.

Example 2: Animated Dashboard Chart

A simple bar chart with hover effects:

require 'victor'
class DashboardChart
def initialize(title, data)
@title = title
@data = data # {label: value, ...}
@svg = Victor::SVG.new(viewBox: '0 0 800 500')
end
def render
@svg.style(type: 'text/css') do
@svg << %{
.bar { fill: #378ADD; transition: fill 0.3s; }
.bar:hover { fill: #e74c3c; }
}
end
@svg.rect(x: 0, y: 0, width: 800, height: 500, fill: '#f9f9f9')
@svg.text(@title, x: 400, y: 30, text_anchor: 'middle', font_size: 24, font_weight: 'bold')
# Axes
@svg.line(x1: 50, y1: 450, x2: 750, y2: 450, stroke: '#333', stroke_width: 2)
@svg.line(x1: 50, y1: 50, x2: 50, y2: 450, stroke: '#333', stroke_width: 2)
# Calculate scaling
max_value = @data.values.max
bar_width = 650 / @data.length
x_pos = 100
@data.each do |label, value|
bar_height = (value / max_value.to_f) * 350
y_pos = 450 - bar_height
@svg.rect(
x: x_pos,
y: y_pos,
width: bar_width - 20,
height: bar_height,
class: 'bar'
)
@svg.text(label, x: x_pos + (bar_width - 20) / 2, y: 470,
text_anchor: 'middle', font_size: 12)
@svg.text(value.to_s, x: x_pos + (bar_width - 20) / 2, y: y_pos - 10,
text_anchor: 'middle', font_size: 11, font_weight: 'bold')
x_pos += bar_width
end
@svg.render
end
end
# Usage
chart = DashboardChart.new('Q4 Sales', { 'Week 1' => 4500, 'Week 2' => 6200, 'Week 3' => 5800, 'Week 4' => 7100 })
puts chart.render

This creates a responsive bar chart with hover interactions.

Conclusion

SVG is the language of modern web graphics. It’s scalable, interactive, lightweight, and accessible. In Ruby, you have multiple tools:

  • Victor for clean, readable code (recommended for most projects)
  • Nokogiri for maximum control and complex XML
  • Builder for Rails integration and performance
  • SVG Gem for focused, simple projects
  • String interpolation for minimal overhead

Raster graphics (ruby-libgd) excel at pixel-level operations, image processing, and satellite imagery. SVG excels at interactive overlays, responsive design, and web delivery. The best modern maps combine both: a raster basemap with SVG vector overlays.

Start with Victor. It’s intuitive, well-documented, and actively maintained. As your needs grow, explore the others. The SVG ecosystem in Ruby is mature, diverse, and ready for production use.

The future of cartography in Ruby is interactive, scalable, and vector-first. SVG is the foundation.

Article content

Leave a comment