Vectored
Vectored is a lightweight, extensible Elixir library for generating SVG images programmatically. It leverages Erlang's built-in :xmerl for XML generation, ensuring no heavy external dependencies while providing a fluent, Elixir-native API.
Pros and Cons
Pros
- Zero Runtime Dependencies: Only depends on Elixir and Erlang/OTP (using built-in
:xmerl). - Fluent API: Easily pipe attribute setters (
with_fill,with_stroke, etc.) to build complex elements. - Extensible: Use the
defelementmacro to create your own SVG elements or implement theVectored.Renderableprotocol. - Accessibility & Interop: Built-in support for
<title>/<desc>anddata-*attributes for frontend framework integration.
Cons
- Low-Level: This library does not provide high-level abstractions (like "BarChart" or "Icon"). It is a thin wrapper over the SVG specification.
- Requires SVG Knowledge: You need to understand how SVG coordinates, viewboxes, and element nesting work.
- No Validation: The library doesn't prevent you from setting invalid attribute values (e.g.,
with_fill("not-a-color")) or nesting elements incorrectly according to the SVG spec. - Verbosity: Building complex scenes involves a lot of Elixir code compared to writing raw XML or using a templating engine.
Installation
Add it to your mix.exs:
def deps do
[
{:vectored, git: "https://github.com/geofflane/vectored.git", tag: "0.4.0"}
]
endQuick Start
alias Vectored.Elements.{Svg, Circle, Rectangle, Stop, LinearGradient}
# Create a simple SVG with a gradient-filled circle
{:ok, svg_string} =
Vectored.new()
|> Svg.with_size(200, 200)
|> Svg.append_defs(
LinearGradient.new([
Stop.new("0%", "red"),
Stop.new("100%", "blue")
]) |> LinearGradient.with_id("my_grad")
)
|> Svg.append(
Circle.new(100, 100, 80)
|> Circle.with_fill("url(#my_grad)")
)
|> Vectored.to_svg_string()Advanced Examples
Complex Paths
Vectored provides a dedicated Path DSL for building complex shapes:
alias Vectored
alias Vectored.Elements.{Path, Svg}
{:ok, svg} =
Vectored.new()
|> Svg.with_view_box(100, 100)
|> Svg.append(fn ->
Path.new()
|> Path.move_to(10, 30)
|> Path.eliptical_arc_curve(20, 20, 0, 0, 1, 50, 30)
|> Path.eliptical_arc_curve(20, 20, 0, 0, 1, 90, 30)
|> Path.quadratic_bezier_curve(90, 60, 50, 90)
|> Path.quadratic_bezier_curve(10, 60, 10, 30)
|> Path.close_path()
end)
|> Vectored.to_svg_string()Creates an SVG image:
Programmatic Example
defmodule FieldDiagram do
alias Vectored
alias Vectored.Elements.{Circle, Defs, Group, Line, Marker, Path, Polyline, Svg, Use}
@width 160
@height 300
@image_los_offset 12
@hash_offset 70.75
@hash_width 2
@hash_stroke 0.5
@doc """
Generate an SVG that is a slice of the field to show motions
"""
@spec generate_svg(number(), number()) :: {:ok, String.t()} | {:error, term()}
def generate_svg(width, height) do
Vectored.new()
|> Svg.with_view_box(width, height)
|> Svg.with_style("background-color: #eee")
|> with_field()
|> Vectored.to_svg_string()
end
# This builds the hashmarks and whatnot
defp with_field(svg) do
Svg.append(svg, fn ->
Group.new()
|> Group.with_id("field")
|> Group.append(yard_markers())
|> Group.append(los())
end)
end
defp los() do
Line.new()
|> Line.from(0, @image_los_offset)
|> Line.to(@width, @image_los_offset)
|> Line.with_stroke("yellow")
|> Line.with_stroke_width(1)
end
def yard_markers() do
Enum.flat_map(0..@height//3, fn y ->
if rem(y, 15) == 0 do
Line.new()
|> Line.from(0, y)
|> Line.to(@width, y)
|> Line.with_stroke("white")
|> Line.with_stroke_width(1)
|> List.wrap()
else
# Hash marks
h1 =
Line.new()
|> Line.from(@hash_offset, y)
|> Line.to(@hash_offset + @hash_width, y)
|> Line.with_stroke("white")
|> Line.with_stroke_width(@hash_stroke)
h2 =
Line.new()
|> Line.from(@width - @hash_offset, y)
|> Line.to(@width - @hash_offset - @hash_width, y)
|> Line.with_stroke("white")
|> Line.with_stroke_width(@hash_stroke)
[h1, h2]
end
end)
end
end
with {:ok, svg} <- FieldDiagram.generate_svg(160, 60) do
File.write!("field.svg", svg)
endSeeing it in Action (Kitchen Sink)
To see a comprehensive demonstration of all supported SVG elements and attributes, you can run the integration "Kitchen Sink" test. This will generate a kitchen_sink.svg file in your project root.
mix test test/vectored/integration/kitchen_sink_test.exs --include manualThis image serves as a visual specification of the library's capabilities, including gradients, masks, symbols, and text styling.
Features & Supported Elements
- Shapes:
Circle,Rectangle,Ellipse,Line,Polyline,Polygon. - Text:
Text,Tspan. - Structure:
Group(g),Defs,Use,Symbol,Marker. - Composition:
ClipPath,Mask. - Aesthetics:
LinearGradient,RadialGradient,Pattern,Image.
TODO
- Validate attributes (types, required, etc)
- More SVG structures
- More extensive tests (test xml output with xpath?)
Copyright and License
Copyright (c) 2024, Geoff Lane.
Source code is licensed under the MIT License.