lib/archeometer/analysis/treemap/rectangle.ex

defmodule Archeometer.Analysis.Treemap.Rectangle do
  @moduledoc """
    This module provides functions to calculate coordinates and size from Archeometer.Analysis.Treemap.Node to be rendered by Archeometer.Analysis.Treemap.SVGrender.
  """
  defstruct coords: {0, 0},
            size: {0, 0},
            drawable_area: {0, 0},
            start_drawable: {0, 0},
            areas: []

  defmodule Area do
    @moduledoc """
    Struct that contains starting coordinates, size and other data  for an area that will be rendered by Archeometer.Analysis.Treemap.SVGRender.
    """
    defstruct [:label, :pct, :begin, :size, :value]
  end

  def create_rectangle(size, coords) do
    %__MODULE__{size: size, drawable_area: size, coords: coords, start_drawable: coords}
  end

  def layout_row(%{drawable_area: {drawable_w, drawable_h}} = rectangle, row, orientation) do
    weights_list = Enum.map(row, & &1.pct)

    {areas, side} =
      case orientation do
        :hor ->
          row_height = Enum.sum(weights_list) / drawable_w

          areas =
            calc_areas(
              rectangle.start_drawable,
              {drawable_w, row_height},
              row,
              orientation
            )

          {areas, row_height}

        :vert ->
          row_width = Enum.sum(weights_list) / drawable_h

          areas =
            calc_areas(
              rectangle.start_drawable,
              {row_width, drawable_h},
              row,
              orientation
            )

          {areas, row_width}
      end

    update_drawable_area(rectangle, side, orientation)
    |> Map.update!(:areas, &(areas ++ &1))
  end

  def row_orientation(%{drawable_area: {d_w, d_h}}) do
    if d_w < d_h do
      :hor
    else
      :vert
    end
  end

  def shortest_drawable_side(%{drawable_area: {d_w, d_h}}), do: min(d_w, d_h)

  defp update_drawable_area(rectangle, row_w, :vert) do
    Map.update!(rectangle, :start_drawable, fn {start_x, start_y} ->
      {start_x + row_w, start_y}
    end)
    |> Map.update!(:drawable_area, fn {drawable_w, drawable_h} ->
      {drawable_w - row_w, drawable_h}
    end)
  end

  defp update_drawable_area(rectangle, row_h, :hor) do
    Map.update!(rectangle, :start_drawable, fn {start_x, start_y} ->
      {start_x, start_y + row_h}
    end)
    |> Map.update!(:drawable_area, fn {drawable_w, drawable_h} ->
      {drawable_w, drawable_h - row_h}
    end)
  end

  defp calc_areas(starting_point, row_dims, row, orientation) do
    Enum.reduce(row, {starting_point, []}, &calc_areas_for_row(&1, &2, row_dims, orientation))
    |> elem(1)
  end

  defp calc_areas_for_row(
         node,
         {{start_x, start_y}, acc},
         {row_w, row_h},
         orientation
       ) do
    node_area = node.pct

    {node_size, new_starting_point} =
      case orientation do
        :hor ->
          node_w = node_area / row_h

          {
            {node_w, row_h},
            {node_w + start_x, start_y}
          }

        :vert ->
          node_h = node_area / row_w

          {
            {row_w, node_h},
            {start_x, node_h + start_y}
          }
      end

    rectangle_area = %__MODULE__.Area{
      begin: {start_x, start_y},
      size: node_size,
      label: node.name,
      pct: node.pct,
      value: node.val
    }

    {new_starting_point, [rectangle_area | acc]}
  end
end