defmodule Contex.SVG do
@moduledoc """
Convenience functions for generating SVG output
"""
def text(x, y, content, opts \\ []) do
attrs = opts_to_attrs(opts)
[
"<text ",
~s|x="#{x}" y="#{y}"|,
attrs,
">",
clean(content),
"</text>"
]
end
def text(content, opts \\ []) do
attrs = opts_to_attrs(opts)
[
"<text ",
attrs,
">",
clean(content),
"</text>"
]
end
def title(content, opts \\ []) do
attrs = opts_to_attrs(opts)
[
"<title ",
attrs,
">",
clean(content),
"</title>"
]
end
def rect({_x1, _x2} = x_extents, {_y1, _y2} = y_extents, inner_content, opts \\ []) do
width = width(x_extents)
height = width(y_extents)
y = min(y_extents)
x = min(x_extents)
attrs = opts_to_attrs(opts)
[
"<rect ",
~s|x="#{x}" y="#{y}" width="#{width}" height="#{height}"|,
attrs,
">",
inner_content,
"</rect>"
]
end
def circle(x, y, radius, opts \\ []) do
attrs = opts_to_attrs(opts)
[
"<circle ",
~s|cx="#{x}" cy="#{y}" r="#{radius}"|,
attrs,
"></circle>"
]
end
def line(points, smoothed, opts \\ []) do
attrs = opts_to_attrs(opts)
path = path(points, smoothed)
[
"<path d=\"",
path,
"\"",
attrs,
"></path>"
]
end
defp path([], _), do: ""
defp path(points, false) do
Enum.reduce(points, :first, fn {x, y}, acc ->
coord = ~s|#{x} #{y}|
case acc do
:first -> ["M ", coord]
_ -> [acc, [" L ", coord]]
end
end)
end
defp path(points, true) do
# Use Catmull-Rom curve - see http://schepers.cc/getting-to-the-point
# First point stays as-is. Subsequent points are draw using SVG cubic-spline
# where control points are calculated as follows:
# - Take the immediately prior data point, the data point itself and the next two into
# an array of 4 points. Where this isn't possible (first & last) duplicate
# Apply Cardinal Spline to Cubic Bezier conversion matrix (this is with tension = 0.0)
# 0 1 0 0
# -1/6 1 1/6 0
# 0 1/6 1 -1/6
# 0 0 1 0
# First control point is second result, second control point is third result, end point is last result
initial_window = {nil, nil, nil, nil}
{_, window, last_p, result} =
Enum.reduce(points, {:first, initial_window, nil, ""}, fn p,
{step, window, last_p, result} ->
case step do
:first ->
{:second, {p, p, p, p}, p, []}
:second ->
{:rest, bump_window(window, p), p, ["M ", coord(last_p)]}
:rest ->
window = bump_window(window, p)
{cp1, cp2} = cardinal_spline_control_points(window)
{:rest, window, p, [result, " C " | [coord(cp1), coord(cp2), coord(last_p)]]}
end
end)
window = bump_window(window, last_p)
{cp1, cp2} = cardinal_spline_control_points(window)
[result, " C " | [coord(cp1), coord(cp2), coord(last_p)]]
end
defp bump_window({_p1, p2, p3, p4}, new_p), do: {p2, p3, p4, new_p}
@spline_tension 0.3
@factor (1.0 - @spline_tension) / 6.0
defp cardinal_spline_control_points({{x1, y1}, {x2, y2}, {x3, y3}, {x4, y4}}) do
cp1 = {x2 + @factor * (x3 - x1), y2 + @factor * (y3 - y1)}
cp2 = {x3 + @factor * (x2 - x4), y3 + @factor * (y2 - y4)}
{cp1, cp2}
end
defp coord({x, y}) do
x = if is_float(x), do: :erlang.float_to_binary(x, decimals: 2), else: x
y = if is_float(y), do: :erlang.float_to_binary(y, decimals: 2), else: y
~s| #{x} #{y}|
end
def opts_to_attrs(opts), do: opts_to_attrs(opts, [])
defp opts_to_attrs([{_, nil} | t], attrs), do: opts_to_attrs(t, attrs)
defp opts_to_attrs([{_, ""} | t], attrs), do: opts_to_attrs(t, attrs)
defp opts_to_attrs([{:phx_click, val} | t], attrs),
do: opts_to_attrs(t, [[" phx-click=\"", val, "\""] | attrs])
defp opts_to_attrs([{:phx_target, val} | t], attrs),
do: opts_to_attrs(t, [[" phx-target=\"", val, "\""] | attrs])
defp opts_to_attrs([{:series, val} | t], attrs),
do: opts_to_attrs(t, [[" phx-value-series=\"", "#{clean(val)}", "\""] | attrs])
defp opts_to_attrs([{:category, val} | t], attrs),
do: opts_to_attrs(t, [[" phx-value-category=\"", "#{clean(val)}", "\""] | attrs])
defp opts_to_attrs([{:value, val} | t], attrs),
do: opts_to_attrs(t, [[" phx-value-value=\"", "#{clean(val)}", "\""] | attrs])
defp opts_to_attrs([{:id, val} | t], attrs),
do: opts_to_attrs(t, [[" phx-value-id=\"", "#{val}", "\""] | attrs])
defp opts_to_attrs([{:task, val} | t], attrs),
do: opts_to_attrs(t, [[" phx-value-task=\"", "#{clean(val)}", "\""] | attrs])
# TODO: This is going to break down with more complex styles
defp opts_to_attrs([{:fill, val} | t], attrs),
do: opts_to_attrs(t, [[" style=\"fill:#", val, ";\""] | attrs])
defp opts_to_attrs([{:transparent, true} | t], attrs),
do: opts_to_attrs(t, [[" fill=\"transparent\""] | attrs])
defp opts_to_attrs([{:stroke, val} | t], attrs),
do: opts_to_attrs(t, [[" stroke=\"#", val, "\""] | attrs])
defp opts_to_attrs([{:stroke_width, val} | t], attrs),
do: opts_to_attrs(t, [[" stroke-width=\"", val, "\""] | attrs])
defp opts_to_attrs([{:stroke_linejoin, val} | t], attrs),
do: opts_to_attrs(t, [[" stroke-linejoin=\"", val, "\""] | attrs])
defp opts_to_attrs([{:opacity, val} | t], attrs),
do: opts_to_attrs(t, [[" fill-opacity=\"", val, "\""] | attrs])
defp opts_to_attrs([{:class, val} | t], attrs),
do: opts_to_attrs(t, [[" class=\"", val, "\""] | attrs])
defp opts_to_attrs([{:transform, val} | t], attrs),
do: opts_to_attrs(t, [[" transform=\"", val, "\""] | attrs])
defp opts_to_attrs([{:text_anchor, val} | t], attrs),
do: opts_to_attrs(t, [[" text-anchor=\"", val, "\""] | attrs])
defp opts_to_attrs([{:dominant_baseline, val} | t], attrs),
do: opts_to_attrs(t, [[" dominant-baseline=\"", val, "\""] | attrs])
defp opts_to_attrs([{:alignment_baseline, val} | t], attrs),
do: opts_to_attrs(t, [[" alignment-baseline=\"", val, "\""] | attrs])
defp opts_to_attrs([{:marker_start, val} | t], attrs),
do: opts_to_attrs(t, [[" marker-start=\"", val, "\""] | attrs])
defp opts_to_attrs([{:marker_mid, val} | t], attrs),
do: opts_to_attrs(t, [[" marker-mid=\"", val, "\""] | attrs])
defp opts_to_attrs([{:marker_end, val} | t], attrs),
do: opts_to_attrs(t, [[" marker-end=\"", val, "\""] | attrs])
defp opts_to_attrs([{key, val} | t], attrs) when is_atom(key),
do: opts_to_attrs(t, [[" ", Atom.to_string(key), "=\"", clean(val), "\""] | attrs])
defp opts_to_attrs([{key, val} | t], attrs) when is_binary(key),
do: opts_to_attrs(t, [[" ", key, "=\"", clean(val), "\""] | attrs])
defp opts_to_attrs([], attrs), do: attrs
defp width({a, b}), do: abs(a - b)
defp min({a, b}), do: min(a, b)
defp clean(s), do: Contex.SVG.Sanitize.basic_sanitize(s)
end