defmodule Contex.SVG do @moduledoc """ Convenience functions for generating SVG output """ def text(x, y, content, opts \\ []) do attrs = opts_to_attrs(opts) [ "", clean(content), "" ] end def text(content, opts \\ []) do attrs = opts_to_attrs(opts) [ "", clean(content), "" ] end def title(content, opts \\ []) do attrs = opts_to_attrs(opts) [ "", clean(content), "" ] 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) [ "", inner_content, "" ] end def circle(x, y, radius, opts \\ []) do attrs = opts_to_attrs(opts) [ "" ] end def line(points, smoothed, opts \\ []) do attrs = opts_to_attrs(opts) path = path(points, smoothed) [ "" ] 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