defmodule Archeometer.Analysis.Treemap.SVGRender do
@moduledoc """
Functions to render an `Archeometer.Analysis.Treemap` struct into a
[svg](https://www.w3schools.com/graphics/svg_intro.asp) image.
`Squarified` algorithm by Mark Bruls, Kees Huizing, and Jarke J. van Wijk: https://www.win.tue.nl/~vanwijk/stm.pdf
"""
require EEx
alias Archeometer.Analysis.Treemap.Rectangle
EEx.function_from_file(
:defp,
:render_svg,
"priv/templates/svg/treemap.svg.eex",
[:rectangles, :ns_sep]
)
@doc """
Renders a `Treemap` struct into a `svg`.
Returns the string representing the generated `svg` image.
The resulting `svg` can be written to a file or directly embedded into an `HTML`.
## Parameters
- `root_node`. The `Treemap` struct with `pct: 100` value to be rendered.
"""
def render(tree, ns_sep \\ ".")
def render(%{pct: pct}, _) when pct != 100 do
{:error, "Root node must equal to 100%"}
end
def render(%{kind: kind}, _) when kind != :group do
{:error, "Root node must be a group"}
end
def render(%{kind: :group, pct: 100} = root_node, ns_sep) do
get_frames(root_node, {10, 10}, {0, 0})
|> List.flatten()
|> render_svg(ns_sep)
|> String.replace(~r/\n\s+\n/, "\n")
end
defp get_frames(%{kind: :group, nodes: nodes}, size, coords) do
ordered_nodes = Enum.sort(nodes, &(&1.pct > &2.pct))
rectangle = squarify(ordered_nodes, [], Rectangle.create_rectangle(size, coords))
groups = Enum.filter(nodes, &(&1.kind == :group))
if Enum.empty?(groups) do
[rectangle]
else
groups_names = Enum.map(groups, & &1.name)
groups_init = Enum.filter(rectangle.areas, &(&1.label in groups_names))
cleared_rectangle =
Map.update!(rectangle, :areas, &Enum.filter(&1, fn x -> x not in groups_init end))
descendant_nodes =
Enum.map(groups, fn node ->
node_data = Enum.find(groups_init, &(&1.label == node.name))
get_frames(node, node_data.size, node_data.begin)
end)
if Enum.empty?(cleared_rectangle.areas) do
descendant_nodes
else
[cleared_rectangle | descendant_nodes]
end
end
end
defp squarify([], row, rectangle) do
orientation = Rectangle.row_orientation(rectangle)
Rectangle.layout_row(rectangle, row, orientation)
end
defp squarify([child | rest], [], rectangle), do: squarify(rest, [child], rectangle)
defp squarify([child | rest], row, rectangle) do
if worst(row, rectangle) > worst([child | row], rectangle) do
squarify(rest, [child | row], rectangle)
else
orientation = Rectangle.row_orientation(rectangle)
rectangle = Rectangle.layout_row(rectangle, row, orientation)
squarify([child | rest], [], rectangle)
end
end
defp worst(row, rectangle) do
weights_list = Enum.map(row, & &1.pct)
min_r = Enum.min(weights_list)
max_r = Enum.max(weights_list)
s = Enum.sum(weights_list)
w = Rectangle.shortest_drawable_side(rectangle)
max(
:math.pow(w, 2) * max_r / :math.pow(s, 2),
:math.pow(s, 2) / (:math.pow(w, 2) * min_r)
)
end
end