diff --git a/lib/matplotex/blueprint/chord.ex b/lib/matplotex/blueprint/chord.ex index c22481f..41940bd 100644 --- a/lib/matplotex/blueprint/chord.ex +++ b/lib/matplotex/blueprint/chord.ex @@ -30,7 +30,10 @@ defmodule Matplotex.Blueprint.Chord do element: [], legend_pos: nil, border: nil, - show_legend: @false_by_default + show_legend: @false_by_default, + region_title: nil, + region_legend: nil, + region_content: nil ] |> Keyword.merge(opts) ) diff --git a/lib/matplotex/element/rad_legend.ex b/lib/matplotex/element/rad_legend.ex index 8f6034c..147b694 100644 --- a/lib/matplotex/element/rad_legend.ex +++ b/lib/matplotex/element/rad_legend.ex @@ -41,19 +41,19 @@ defmodule Matplotex.Element.RadLegend do x: x, y: y, width: width, - height: height, - label_margin: label_margin - } = legend + height: height + } = legend, legend_font ) do + %{ legend | label: %Label{ - x: x + width + label_margin, - y: y - height / 4, + x: x + width , + y: y + height / 2, text: text, - type: @label_type, - text_anchor: "start" + type: @label_type } + |> Label.cast_label(legend_font) } end end diff --git a/lib/matplotex/figure.ex b/lib/matplotex/figure.ex index ff375f2..ad3d563 100644 --- a/lib/matplotex/figure.ex +++ b/lib/matplotex/figure.ex @@ -31,6 +31,10 @@ defmodule Matplotex.Figure do def new(opts) do struct(__MODULE__, opts) end + # TODO: put error message in error + # def put_error(figure, opts) do + + # end def add_label(%__MODULE__{axes: %module{} = axes} = figure, label, opts), do: %{figure | axes: module.add_label(axes, label, opts)} @@ -110,4 +114,6 @@ defmodule Matplotex.Figure do defp update_rc_params(_, _) do raise Matplotex.InputError, keys: [:rc_params], message: "Invalid Input" end + + end diff --git a/lib/matplotex/figure/areal.ex b/lib/matplotex/figure/areal.ex index 1aadc7b..92e4407 100644 --- a/lib/matplotex/figure/areal.ex +++ b/lib/matplotex/figure/areal.ex @@ -30,7 +30,10 @@ defmodule Matplotex.Figure.Areal do alias Matplotex.Figure.Dataset alias Matplotex.Figure.Text + alias Matplotex.Figure.Areal.Region + alias Matplotex.Figure.RcParams @default_tick_minimum 0 + @zero_to_move 0 def add_label(%__MODULE__{label: nil} = axes, {key, label}, opts) when is_binary(label) do label = Map.new() @@ -143,7 +146,7 @@ defmodule Matplotex.Figure.Areal do def materialized_by_region(figure) do figure - |> Lead.set_regions() + |> Lead.set_regions_areal() |> Cast.cast_xticks_by_region() |> Cast.cast_yticks_by_region() |> Cast.cast_hgrids_by_region() @@ -211,11 +214,119 @@ defmodule Matplotex.Figure.Areal do def set_frame_size(%__MODULE__{} = axes, frame_size) do %__MODULE__{axes | size: frame_size} end + + def set_region_title( + %Figure{ + axes: + %{ + title: title, + region_x: %Region{width: region_x_width}, + region_y: %Region{height: region_y_height} = region_y, + region_title: region_title, + size: {_f_width, _f_height}, + border: {lx, _by, _, ty} + } = axes, + rc_params: %RcParams{title_font: title_font} + } = figure + ) do + space_for_title = Lead.height_required_for_text(title_font, title) + + {x_region_title, y_region_title} = + Algebra.transform_given_point(@zero_to_move, -space_for_title, lx, ty) + + %Figure{ + figure + | axes: %{ + axes + | region_title: %Region{ + region_title + | x: x_region_title, + y: y_region_title + space_for_title, + width: region_x_width, + height: space_for_title + }, + region_y: %Region{ + region_y + | height: region_y_height - space_for_title + } + } + } + end + + def set_region_title(figure), do: figure + + + def set_region_legend( + %Figure{ + axes: + %{ + show_legend: true, + region_x: %Region{width: region_x_width} = region_x, + region_title: %Region{height: region_title_height}, + region_legend: region_legend, + size: {f_width, _f_height}, + border: {_lx, by, rx, ty} + } = axes, + rc_params: %RcParams{legend_width: legend_width} + } = figure + ) do + region_legend_width = f_width * legend_width + region_x_width_after_legend = region_x_width - region_legend_width + + {x_region_legend, y_region_legend} = + Algebra.transform_given_point(-region_legend_width, -region_title_height, rx, ty, 0) + + %Figure{ + figure + | axes: %{ + axes + | region_x: %Region{ + region_x + | width: region_x_width_after_legend + }, + region_legend: %Region{ + region_legend + | x: x_region_legend, + y: y_region_legend, + width: region_legend_width, + height: y_region_legend - by + } + } + } + end + def set_region_legend(figure), do: figure + def set_region_content( + %Figure{ + axes: + %{ + region_x: %Region{x: x_region_x, width: region_x_width}, + region_y: %Region{y: y_region_y, height: region_y_height}, + region_content: region_content + } = axes + } = figure + ) do + %Figure{ + figure + | axes: %{ + axes + | region_content: %Region{ + region_content + | x: x_region_x, + y: y_region_y, + width: region_x_width, + height: region_y_height + } + } + } + end + def set_region_content(figure), do: figure end end + def transformation({_labelx, x}, {_labely, y}, xminmax, yminmax, width, height, transition) do transformation(x, y, xminmax, yminmax, width, height, transition) end + def transformation({_label, x}, y, xminmax, yminmax, width, height, transition) do transformation(x, y, xminmax, yminmax, width, height, transition) end @@ -224,7 +335,6 @@ defmodule Matplotex.Figure.Areal do transformation(x, y, xminmax, yminmax, width, height, transition) end - def transformation( x, y, @@ -246,11 +356,11 @@ defmodule Matplotex.Figure.Areal do x |> Enum.zip(y) |> Enum.map(fn {x, y} -> - x |> transformation(y, xlim, ylim, width, height, transition) |> Algebra.flip_y_coordinate() end) + %Dataset{dataset | transformed: transformed} end end diff --git a/lib/matplotex/figure/areal/bar_chart.ex b/lib/matplotex/figure/areal/bar_chart.ex index c9ac286..414f22d 100644 --- a/lib/matplotex/figure/areal/bar_chart.ex +++ b/lib/matplotex/figure/areal/bar_chart.ex @@ -35,7 +35,12 @@ defmodule Matplotex.Figure.Areal.BarChart do dataset = Dataset.cast(%Dataset{x: x, y: values, pos: pos, width: width}, opts) datasets = data ++ [dataset] xydata = flatten_for_data(datasets) - %Figure{figure | rc_params: %RcParams{white_space: width, y_padding: 0}, axes: %{axes | data: xydata, dataset: datasets}} + + %Figure{ + figure + | rc_params: %RcParams{white_space: width, y_padding: 0}, + axes: %{axes | data: xydata, dataset: datasets} + } end @impl Areal @@ -146,7 +151,7 @@ defmodule Matplotex.Figure.Areal.BarChart do defp hypox(y) do nof_x = length(y) - @xmin_value|>Nx.linspace(nof_x, n: nof_x)|>Nx.to_list() + @xmin_value |> Nx.linspace(nof_x, n: nof_x) |> Nx.to_list() end defp bar_position(x, pos_factor) when pos_factor < 0 do diff --git a/lib/matplotex/figure/areal/region.ex b/lib/matplotex/figure/areal/region.ex index e4ad9f6..83a1836 100644 --- a/lib/matplotex/figure/areal/region.ex +++ b/lib/matplotex/figure/areal/region.ex @@ -1,7 +1,13 @@ defmodule Matplotex.Figure.Areal.Region do alias Matplotex.Figure.Areal.XyRegion.Coords @zero_by_default 0 - defstruct x: @zero_by_default, y: @zero_by_default, width: @zero_by_default, height: @zero_by_default, name: nil, theta: @zero_by_default, coords: nil + defstruct x: @zero_by_default, + y: @zero_by_default, + width: @zero_by_default, + height: @zero_by_default, + name: nil, + theta: @zero_by_default, + coords: nil def get_label_coords(%__MODULE__{x: x, y: y, coords: %Coords{label: {label_x, label_y}}}) do {x + label_x, y + label_y} diff --git a/lib/matplotex/figure/cast.ex b/lib/matplotex/figure/cast.ex index 73b8dcb..2632cb9 100644 --- a/lib/matplotex/figure/cast.ex +++ b/lib/matplotex/figure/cast.ex @@ -138,6 +138,8 @@ defmodule Matplotex.Figure.Cast do rc_params: %RcParams{line_width: line_width} } = figure ) do + {lx, by} = Algebra.flip_y_coordinate({lx, by}) + {rx, ty} = Algebra.flip_y_coordinate({rx, ty}) left = %Line{x1: lx, y1: by, x2: lx, y2: ty, type: "border.left", stroke_width: line_width} right = %Line{x1: rx, y1: by, x2: rx, y2: ty, type: "border.right", stroke_width: line_width} top = %Line{x1: lx, x2: rx, y1: ty, y2: ty, type: "border.top", stroke_width: line_width} @@ -158,14 +160,14 @@ defmodule Matplotex.Figure.Cast do %Figure{ axes: %{ - coords: %Coords{title: title_coord} = coords, + region_title: region_title, title: title, element: elements } = axes, rc_params: %RcParams{title_font: title_font} } = figure ) do - {ttx, tty} = calculate_center(coords, title_coord, :x) + {ttx, tty} = calculate_center(region_title, :x) title = %Label{ @@ -189,14 +191,15 @@ defmodule Matplotex.Figure.Cast do %Figure{ axes: %{ - region_title: region, + region_title: region_title, title: title, element: elements } = axes, rc_params: %RcParams{title_font: title_font, label_padding: title_padding} } = figure ) do - {title_x, title_y} = region |> calculate_center(:x) |> Algebra.flip_y_coordinate() + + {title_x, title_y} = region_title |> calculate_center(:x) |> Algebra.flip_y_coordinate() title = %Label{ @@ -430,7 +433,6 @@ defmodule Matplotex.Figure.Cast do } = figure ) when is_list(x_ticks) do - x_ticks = confine_ticks(x_ticks, xlim) x_data = confine_data(x_data, xlim) dataset = confine_data(dataset, xlim, :x) @@ -448,12 +450,12 @@ defmodule Matplotex.Figure.Cast do Enum.map(ticks_width_position, fn {tick_position, label} -> {_, y_x_tick_line} = @zero_to_move - |>Algebra.transform_given_point(height_region_x, x_region_x, y_region_x) + |> Algebra.transform_given_point(height_region_x, x_region_x, y_region_x) |> Algebra.flip_y_coordinate() {x_x_tick, y_x_tick} = tick_position - |>Algebra.transform_given_point( + |> Algebra.transform_given_point( @zero_to_move, x_region_x_with_padding, y_x_tick @@ -480,6 +482,7 @@ defmodule Matplotex.Figure.Cast do {%Tick{type: @xtick_type, tick_line: line, label: label}, {x_x_tick, y_x_tick_line}} end) |> Enum.unzip() + elements = elements ++ x_tick_elements %Figure{ @@ -498,6 +501,7 @@ defmodule Matplotex.Figure.Cast do defp format_tick_label({label, _index}), do: label defp format_tick_label(label) when is_float(label), do: Float.round(label, 2) defp format_tick_label(label), do: label + defp content_linespace(number_of_ticks_required, axis_size) do @lowest_tick |> Nx.linspace(axis_size, n: number_of_ticks_required) |> Nx.to_list() end @@ -630,7 +634,7 @@ defmodule Matplotex.Figure.Cast do {x_y_tick, y_y_tick} = @zero_to_move - |>Algebra.transform_given_point( + |> Algebra.transform_given_point( tick_position, x_y_tick, y_region_y_with_padding diff --git a/lib/matplotex/figure/lead.ex b/lib/matplotex/figure/lead.ex index 28ff82b..0528aff 100644 --- a/lib/matplotex/figure/lead.ex +++ b/lib/matplotex/figure/lead.ex @@ -4,26 +4,36 @@ defmodule Matplotex.Figure.Lead do alias Matplotex.Figure.Font alias Matplotex.Figure.Areal.Region alias Matplotex.Figure.TwoD - alias Matplotex.Figure.Coords alias Matplotex.Figure.RcParams alias Matplotex.Figure - @pt_to_inch 1 / 150 - @padding 10 / 96 @zero_to_move 0 - @spec set_regions(Matplotex.Figure.t()) :: Matplotex.Figure.t() + @spec set_regions_areal(Matplotex.Figure.t()) :: Matplotex.Figure.t() - def set_regions(%Figure{figsize: {width, height}} = figure) when width > 0 and height > 0 do + def set_regions_areal(%Figure{figsize: {width, height}, axes: %module{}} = figure) + when width > 0 and height > 0 do figure |> set_frame_size() |> ensure_ticks_are_valid() |> set_region_xy() - |> set_region_title() - |> set_region_legend() - |> set_region_content() + |> module.set_region_title() + |> module.set_region_legend() + |> module.set_region_content() end - def set_regions(figure), do: figure + def set_regions_areal(figure), do: figure + + def set_regions_radial(%Figure{figsize: {width, height}, axes: %module{}} = figure) + when width > 0 and height > 0 do + figure + |> set_frame_size() + |> module.set_region_title() + |> module.set_region_legend() + |> module.set_region_content() + |> focus_to_origin() + end + + def set_regions_radial(figure), do: figure def set_border(%Figure{margin: margin, axes: axes, figsize: {fig_width, fig_height}} = figure) do margin = margin / 2 @@ -56,6 +66,7 @@ defmodule Matplotex.Figure.Lead do ) do {x_ticks, x_lim} = maybe_generate_ticks(x_ticks, x_lim, x_data, width) {y_ticks, y_lim} = maybe_generate_ticks(y_ticks, y_lim, y_data, height) + %Figure{ figure | axes: %{ @@ -131,10 +142,17 @@ defmodule Matplotex.Figure.Lead do {x_region_y, y_region_y} = Algebra.transform_given_point(@zero_to_move, space_required_for_region_x, lx, by) - x_label_coords = Algebra.transform_given_point(@zero_to_move, @zero_to_move, x_region_x, y_region_x) - x_tick_coords = Algebra.transform_given_point(@zero_to_move, space_for_x_label, x_region_x, y_region_x) - y_label_coords = Algebra.transform_given_point(@zero_to_move, @zero_to_move, x_region_y, y_region_y) - y_tick_coords = Algebra.transform_given_point(space_for_ylabel, @zero_to_move, x_region_y, y_region_y) + x_label_coords = + Algebra.transform_given_point(@zero_to_move, @zero_to_move, x_region_x, y_region_x) + + x_tick_coords = + Algebra.transform_given_point(@zero_to_move, space_for_x_label, x_region_x, y_region_x) + + y_label_coords = + Algebra.transform_given_point(@zero_to_move, @zero_to_move, x_region_y, y_region_y) + + y_tick_coords = + Algebra.transform_given_point(space_for_ylabel, @zero_to_move, x_region_y, y_region_y) %Figure{ figure @@ -160,183 +178,96 @@ defmodule Matplotex.Figure.Lead do } end - defp set_region_xy(figure), do: figure - - defp set_region_title( - %Figure{ - axes: - %{ - title: title, - region_x: %Region{width: region_x_width}, - region_y: %Region{height: region_y_height} = region_y, - region_title: region_title, - size: {_f_width, _f_height}, - border: {lx, _by, _, ty} - } = axes, - rc_params: %RcParams{title_font: title_font} - } = figure - ) do - space_for_title = height_required_for_text(title_font, title) - - {x_region_title, y_region_title} = - Algebra.transform_given_point(@zero_to_move, -space_for_title, lx, ty, 0) - - %Figure{ - figure - | axes: %{ - axes - | region_title: %Region{ - region_title - | x: x_region_title, - y: y_region_title + space_for_title, - width: region_x_width, - height: space_for_title - }, - region_y: %Region{ - region_y - | height: region_y_height - space_for_title - } - } - } - end - - defp set_region_legend( - %Figure{ - axes: - %{ - show_legend: true, - region_x: %Region{width: region_x_width} = region_x, - region_title: %Region{height: region_title_height}, - region_legend: region_legend, - size: {f_width, _f_height}, - border: {_lx, by, rx, ty} - } = axes, - rc_params: %RcParams{legend_width: legend_width} - } = figure - ) do - region_legend_width = f_width * legend_width - region_x_width_after_legend = region_x_width - region_legend_width - - {x_region_legend, y_region_legend} = - Algebra.transform_given_point(-region_legend_width, -region_title_height, rx, ty, 0) - - %Figure{ - figure - | axes: %{ - axes - | region_x: %Region{ - region_x - | width: region_x_width_after_legend - }, - region_legend: %Region{ - region_legend - | x: x_region_legend, - y: y_region_legend, - width: region_legend_width, - height: y_region_legend - by - } - } - } - end - - defp set_region_legend(figure), do: figure - - defp set_region_content( - %Figure{ - axes: - %{ - region_x: %Region{x: x_region_x, width: region_x_width}, - region_y: %Region{y: y_region_y, height: region_y_height}, - region_content: region_content - } = axes - } = figure - ) do - %Figure{ - figure - | axes: %{ - axes - | region_content: %Region{ - region_content - | x: x_region_x, - y: y_region_y, - width: region_x_width, - height: region_y_height - } - } - } - end - - defp set_region_content(figure), do: figure - - def focus_to_origin( - %Figure{ - figsize: {width, height}, - margin: margin, - rc_params: %RcParams{title_font_size: title_font_size}, - axes: %{title: title} = axes - } = figure - ) do - leftx = width * margin - bottomy = height * margin - rightx = width - width * margin - topy = height - height * margin - title_coords = {leftx, topy} - title_offset = label_offset(title, title_font_size) - topy = topy - title_offset - inner_size = {width, height} = {width - 2 * leftx, height - 2 * bottomy - title_offset} - - {{centerx, centery}, radius} = - center_and_radius(width, height, {leftx, rightx, bottomy, topy}) - - coords = %Coords{ - title: title_coords, - bottom_left: {leftx, bottomy}, - top_left: {leftx, topy}, - bottom_right: {rightx, bottomy}, - top_right: {rightx, topy} - } + def focus_to_origin(%Figure{rc_params: %RcParams{padding: padding}, axes: %{region_content: %Region{x: x_region_content, y: y_region_content, width: width_region_content, height: height_region_content}}=axes}=figure) do + width_padding_value = width_region_content * padding + height_padding_value = height_region_content * padding + radius = plotable_radius(width_region_content, height_region_content, padding) + {center_x, center_y} = x_region_content|>Algebra.transform_given_point(y_region_content, width_padding_value, height_padding_value)|>Algebra.transform_given_point({radius, radius}) %Figure{ figure | axes: %{ axes | radius: radius, - center: %TwoD{x: centerx, y: centery}, - coords: coords, - size: inner_size, - legend_pos: {2 * leftx + 2 * radius, topy} + center: %TwoD{x: center_x, y: center_y} } } end - - defp center_and_radius(width, height, {leftx, _rightx, bottomy, _topy}) when height < width do - radius = height / 2 - - centerx = leftx + radius - centery = bottomy + radius - {{centerx, centery}, radius} - end - - defp center_and_radius(width, _height, {leftx, _rightx, _bottomy, topy}) do - radius = width / 2 - - centerx = leftx + radius - centery = topy - radius - {{centerx, centery}, radius} - end - - - - defp label_offset(nil, _font_size), do: 0 - defp label_offset("", _font_size), do: 0 - - defp label_offset(ticks, font_size) when is_list(ticks) do - font_size * @pt_to_inch + @padding + def focus_to_origin(figure), do: figure + defp plotable_radius(width, height, padding) when height < width do + (height - height * padding * 2) / 2 end + defp plotable_radius(width, height, padding) when width < height do + (width - width * padding * 2) / 2 + end + # def focus_to_origin( + # %Figure{ + # figsize: {width, height}, + # margin: margin, + # rc_params: %RcParams{title_font_size: title_font_size}, + # axes: %{title: title} = axes + # } = figure + # ) do + # leftx = width * margin + # bottomy = height * margin + # rightx = width - width * margin + # topy = height - height * margin + # title_coords = {leftx, topy} + # title_offset = label_offset(title, title_font_size) + # topy = topy - title_offset + # inner_size = {width, height} = {width - 2 * leftx, height - 2 * bottomy - title_offset} + + # {{centerx, centery}, radius} = + # center_and_radius(width, height, {leftx, rightx, bottomy, topy}) + + # coords = %Coords{ + # title: title_coords, + # bottom_left: {leftx, bottomy}, + # top_left: {leftx, topy}, + # bottom_right: {rightx, bottomy}, + # top_right: {rightx, topy} + # } + + # %Figure{ + # figure + # | axes: %{ + # axes + # | radius: radius, + # center: %TwoD{x: centerx, y: centery}, + # coords: coords, + # size: inner_size, + # legend_pos: {2 * leftx + 2 * radius, topy} + # } + # } + # end + + # defp center_and_radius(width, height, {leftx, _rightx, bottomy, _topy}) when height < width do + # radius = height / 2 + + # centerx = leftx + radius + # centery = bottomy + radius + # {{centerx, centery}, radius} + # end + + # defp center_and_radius(width, _height, {leftx, _rightx, _bottomy, topy}) do + # radius = width / 2 + + # centerx = leftx + radius + # centery = topy - radius + # {{centerx, centery}, radius} + # end + + # defp label_offset(nil, _font_size), do: 0 + # defp label_offset("", _font_size), do: 0 + + # defp label_offset(ticks, font_size) when is_list(ticks) do + # font_size * @pt_to_inch + @padding + # end + + # defp label_offset(_label, font_size) do + # font_size * @pt_to_inch + @padding + # end - defp label_offset(_label, font_size) do - font_size * @pt_to_inch + @padding - end defp tick_length(tick) when is_integer(tick) do tick |> Integer.to_string() |> String.length() end @@ -346,26 +277,29 @@ defmodule Matplotex.Figure.Lead do end defp tick_length(tick) when is_float(tick) do - tick|>Float.round(2) |> Float.to_string() |> String.length() + tick |> Float.round(2) |> Float.to_string() |> String.length() end defp tick_length({label, _v}) when is_binary(label) do String.length(label) end - defp height_required_for_text( - %Font{ - font_size: font_size, - pt_to_inch_ratio: pt_to_inch_ratio, - flate: flate, - rotation: 0 - }, - _text - ) do + @doc """ + Calculates the height required for a given text with given font details + """ + def height_required_for_text( + %Font{ + font_size: font_size, + pt_to_inch_ratio: pt_to_inch_ratio, + flate: flate, + rotation: 0 + }, + _text + ) do to_number(font_size) * pt_to_inch_ratio + flate end - defp height_required_for_text( + def height_required_for_text( %Font{ font_size: font_size, pt_to_inch_ratio: pt_to_inch_ratio, @@ -381,27 +315,31 @@ defmodule Matplotex.Figure.Lead do text_height + height_for_rotation + flate end - defp length_required_for_text( - %Font{ - font_size: font_size, - pt_to_inch_ratio: pt_to_inch_ratio, - flate: flate, - rotation: 0 - }, - text - ), - do: tick_length(text) * to_number(font_size) * (pt_to_inch_ratio/2) + flate - - defp length_required_for_text( - %Font{ - font_size: font_size, - pt_to_inch_ratio: pt_to_inch_ratio, - flate: flate, - rotation: rotation - }, - text - ) do - text_length = tick_length(text) * to_number(font_size) * (pt_to_inch_ratio/2) + @doc """ + Length required for a text string with given font details + """ + + def length_required_for_text( + %Font{ + font_size: font_size, + pt_to_inch_ratio: pt_to_inch_ratio, + flate: flate, + rotation: 0 + }, + text + ), + do: tick_length(text) * to_number(font_size) * (pt_to_inch_ratio / 2) + flate + + def length_required_for_text( + %Font{ + font_size: font_size, + pt_to_inch_ratio: pt_to_inch_ratio, + flate: flate, + rotation: rotation + }, + text + ) do + text_length = tick_length(text) * to_number(font_size) * (pt_to_inch_ratio / 2) rotation = deg_to_rad(rotation) leng_for_rotation = :math.cos(rotation) * text_length leng_for_rotation + flate diff --git a/lib/matplotex/figure/radial.ex b/lib/matplotex/figure/radial.ex index c039ee4..9cdc586 100644 --- a/lib/matplotex/figure/radial.ex +++ b/lib/matplotex/figure/radial.ex @@ -1,4 +1,6 @@ defmodule Matplotex.Figure.Radial do + alias Matplotex.Utils.Algebra + @callback create(struct(), list(), keyword()) :: struct() @callback materialize(struct()) :: struct() @@ -16,6 +18,12 @@ defmodule Matplotex.Figure.Radial do alias Matplotex.Figure.Text alias Matplotex.Figure.Lead alias Matplotex.Figure.Cast + alias Matplotex.Figure.RcParams + alias Matplotex.Figure + alias Matplotex.Figure.Areal.Region + alias Matplotex.Utils.Algebra + + @zero_to_move 0 def add_title(axes, title, opts) when is_binary(title) do %{axes | title: title, show_title: true} @@ -23,11 +31,106 @@ defmodule Matplotex.Figure.Radial do def materialized(figure) do figure - |> Lead.focus_to_origin() - |> Lead.set_border() + |> Lead.set_regions_radial() |> Cast.cast_border() - |> Cast.cast_title() + |> Cast.cast_title_by_region() + end + + def set_region_title( + %Figure{ + axes: + %{ + title: title, + center: _center, + border: {lx, _by, _rx, ty}, + size: {width, _height} + } = + axes, + rc_params: %RcParams{title_font: title_font} + } = figure + ) do + space_for_title = Lead.height_required_for_text(title_font, title) + + {x_region_title, y_region_title} = + Algebra.transform_given_point(@zero_to_move, -space_for_title, lx, ty) + + %Figure{ + figure + | axes: %{ + axes + | region_title: %Region{ + x: x_region_title, + y: y_region_title, + width: width, + height: space_for_title + } + } + } + end + def set_region_title(figure), do: figure + def set_region_legend( + %Figure{ + figsize: {fwidth, _fheight}, + rc_params: %RcParams{legend_width: legend_width}, + axes: + %{ + border: {_lx, by, rx, ty}, + show_legend: true, + region_title: %Region{y: y_region_title, height: height_region_title} + } = axes + } = figure + ) do + width_region_legend = fwidth * legend_width + height_region_legend = abs(by - y_region_title) + + {x_region_legend, y_region_legend} = + Algebra.transform_given_point(rx, abs(ty), -width_region_legend,height_region_title)|>Algebra.flip_y_coordinate() + + %Figure{ + figure + | axes: %{ + axes + | region_legend: %Region{ + x: x_region_legend, + y: y_region_legend, + width: width_region_legend, + height: height_region_legend + } + } + } end + def set_region_legend(figure), do: figure + + def set_region_content( + %Figure{ + axes: + %{ + border: {lx, by, _rx, _ty}, + region_title: %Region{height: height_region_title}, + region_legend: %Region{width: width_region_legend}, + size: {width, height} + } = axes + } = figure + ) do + + width_region_content = width - width_region_legend + height_region_content = height - height_region_title + + %Figure{ + figure + | axes: %{ + axes + | region_content: %Region{ + x: lx, + y: by, + width: width_region_content, + height: height_region_content + } + } + } + end + + def set_region_content(figure), do: figure end end end diff --git a/lib/matplotex/figure/radial/pie.ex b/lib/matplotex/figure/radial/pie.ex index 618509d..e69a158 100644 --- a/lib/matplotex/figure/radial/pie.ex +++ b/lib/matplotex/figure/radial/pie.ex @@ -1,4 +1,8 @@ defmodule Matplotex.Figure.Radial.Pie do + + alias Matplotex.Figure.RcParams + alias Matplotex.Utils.Algebra + alias Matplotex.Figure.Areal.Region alias Matplotex.Figure.Radial alias Matplotex.Element.RadLegend alias Matplotex.Figure.Radial.LegendAcc @@ -12,10 +16,22 @@ defmodule Matplotex.Figure.Radial.Pie do use Radial @full_circle 2 * :math.pi() - chord(center: %TwoD{}, lead: %TwoD{}, coords: %Coords{}) + chord( + center: %TwoD{}, + lead: %TwoD{}, + coords: %Coords{}, + region_title: %Region{}, + region_legend: %Region{}, + region_content: %Region{} + ) + @impl Radial def create(%Figure{axes: axes} = figure, sizes, opts) do - dataset = Dataset.cast(%Dataset{sizes: sizes}, opts) + dataset = if sizes|>Enum.sum()|>abs() > 0 do + Dataset.cast(%Dataset{sizes: sizes}, opts) + else + raise Matplotex.InputError, "Invalid set of values for a pie chart, sum of sizes should be greater than 0" + end %Figure{figure | axes: %{axes | dataset: dataset}} end @@ -28,13 +44,14 @@ defmodule Matplotex.Figure.Radial.Pie do defp materialize_slices( %Figure{ - figsize: {_fwidth, fheight}, + figsize: {fwidth, fheight}, + rc_params: %RcParams{legend_font: legend_font}, axes: %__MODULE__{ size: {_width, height}, radius: radius, - center: %{y: cy} = center, - legend_pos: {legx, legy}, + center: center, + region_legend: region_legend, dataset: %Dataset{ sizes: sizes, labels: labels, @@ -44,11 +61,11 @@ defmodule Matplotex.Figure.Radial.Pie do element: elements } = axes } = figure - ) do + ) when fwidth > 0 and fheight > 0 do + %Region{x: legx, y: legy} = Algebra.flip_y_coordinate(region_legend) total_size = Enum.sum(sizes) legend_rect_side = height / length(sizes) / 2 - center = %{center | y: fheight - cy} - + center = Algebra.flip_y_coordinate(center) slices = sizes |> Enum.zip(labels) @@ -65,7 +82,7 @@ defmodule Matplotex.Figure.Radial.Pie do } }, fn raw, color, acc -> - roll_across(raw, color, acc, center, radius, total_size) + roll_across(raw, color, acc, center, radius, total_size, legend_font) end ) |> then(fn %Accumulator{slices: slices, legends: legends} -> @@ -99,7 +116,8 @@ defmodule Matplotex.Figure.Radial.Pie do }, %{x: cx, y: cy} = center, radius, - total_size + total_size, + legend_font ) do percentage = size / total_size angle_for_size = percentage * @full_circle + start_angle @@ -119,7 +137,7 @@ defmodule Matplotex.Figure.Radial.Pie do cy: cy } - y_legend = y_legend - legend_unit_height + {x_legend, y_legend} = Algebra.transform_given_point(0, legend_unit_height, x_legend, y_legend) legend = %RadLegend{ @@ -131,7 +149,7 @@ defmodule Matplotex.Figure.Radial.Pie do height: legend_unit_height, label: "#{label}-#{Float.ceil(percentage * 100, 2)}%" } - |> RadLegend.with_label() + |> RadLegend.with_label(legend_font) %Accumulator{ lead: {x2, y2}, diff --git a/lib/matplotex/figure/rc_params.ex b/lib/matplotex/figure/rc_params.ex index 1f963c2..5308a80 100644 --- a/lib/matplotex/figure/rc_params.ex +++ b/lib/matplotex/figure/rc_params.ex @@ -23,8 +23,8 @@ defmodule Matplotex.Figure.RcParams do y_tick_font: %Font{text_anchor: "start"}, x_label_font: @font, y_label_font: @font, - title_font: %Font{font_size: @default_title_font_size}, - legend_font: @font, + title_font: %Font{font_size: @default_title_font_size, dominant_baseline: "auto"}, + legend_font: %Font{text_anchor: "start"}, figure_size: @default_figsize, figure_dpi: @default_dpi, line_width: @line_width, @@ -44,6 +44,7 @@ defmodule Matplotex.Figure.RcParams do tick_line_length: @tick_line_length, x_padding: @chart_padding, y_padding: @chart_padding, + padding: @chart_padding, white_space: 0, label_padding: @label_padding, legend_width: @default_legend_width_percentage, diff --git a/lib/matplotex/helpers.ex b/lib/matplotex/helpers.ex index cbe8d5a..a9e2784 100644 --- a/lib/matplotex/helpers.ex +++ b/lib/matplotex/helpers.ex @@ -212,7 +212,7 @@ defmodule Matplotex.Helpers do |> Matplotex.plot(x, y2, color: "red", linestyle: "--", marker: "^", label: "Dataset 2") |> Matplotex.plot(x, y3, color: "green", linestyle: "-.", marker: "s", label: "Dataset 3") |> Matplotex.set_title("Title") - |> Matplotex.set_xticks([1,2,3,4,5]) + |> Matplotex.set_xticks([1, 2, 3, 4, 5]) |> Matplotex.set_xlabel("X-Axis") |> Matplotex.set_ylabel("Y-Axis") |> Matplotex.show() @@ -270,7 +270,8 @@ defmodule Matplotex.Helpers do sizes |> Matplotex.pie(colors: colors, labels: labels) |> Matplotex.set_title("Pie chart") - |> Matplotex.figure(%{figsize: {3.5, 2.5}, margin: 0.05}) + |> Matplotex.figure(%{figsize: {4, 3}, margin: 0.05}) + |> Matplotex.set_rc_params(%{line_width: 1, legend_width: 0.25}) |> Matplotex.show() |> copy() end @@ -305,7 +306,7 @@ defmodule Matplotex.Helpers do values |> Matplotex.pie(colors: colors, labels: categories) |> Matplotex.set_title("Asias Emission distribution(2008-2011)") - |> Matplotex.set_rc_params(%{line_width: 1}) + |> Matplotex.set_rc_params(%{line_width: 1, legend_width: 0.5}) |> Matplotex.figure(%{figsize: {10, 4}, margin: 0.15}) |> Matplotex.show() |> copy() diff --git a/lib/matplotex/utils/algebra.ex b/lib/matplotex/utils/algebra.ex index 1fde07f..182fcbd 100644 --- a/lib/matplotex/utils/algebra.ex +++ b/lib/matplotex/utils/algebra.ex @@ -51,6 +51,7 @@ defmodule Matplotex.Utils.Algebra do def transform_given_point(x, y, sx, sy, tx, ty, theta \\ 0) do point_matrix = Nx.tensor([x, y, 1], type: {:f, @tensor_data_type_bits}) + Nx.tensor( [ [sx * :math.cos(theta), sy * -:math.sin(theta), tx], @@ -64,9 +65,12 @@ defmodule Matplotex.Utils.Algebra do |> List.to_tuple() |> then(fn {x, y, _} -> {x, y} end) end - + def transform_given_point({x, y}, {ox, oy}, theta \\ 0) do + transform_given_point(x, y, ox, oy, theta) + end def transform_given_point(x, y, ox, oy, theta \\ 0) do point_matrix = Nx.tensor([x, y, 1], type: {:f, @tensor_data_type_bits}) + Nx.tensor( [ [:math.cos(theta), -:math.sin(theta), ox], @@ -81,7 +85,13 @@ defmodule Matplotex.Utils.Algebra do |> then(fn {x, y, _} -> {x, y} end) end + + def flip_y_coordinate({x, y}) do {x, -y} end + + def flip_y_coordinate(%{y: y} = point) do + %{point | y: -y} + end end diff --git a/test/matplotex/figure/cast_test.exs b/test/matplotex/figure/cast_test.exs index f78d994..fd58245 100644 --- a/test/matplotex/figure/cast_test.exs +++ b/test/matplotex/figure/cast_test.exs @@ -9,10 +9,9 @@ defmodule Matplotex.Figure.CastTest do {:ok, %{figure: figure}} end - describe "cast_spines_by_region/1" do test "add elements for borders in axes", %{figure: figure} do - figure = Lead.set_regions(figure) + figure = Lead.set_regions_areal(figure) assert %Figure{axes: %{element: elements}} = Cast.cast_spines_by_region(figure) assert Enum.any?(elements, fn x -> x.type == "spine.top" end) @@ -26,7 +25,7 @@ defmodule Matplotex.Figure.CastTest do test "add element for title in axes", %{figure: figure} do assert %Figure{axes: %{element: elements}} = figure - |> Lead.set_regions() + |> Lead.set_regions_areal() |> Cast.cast_spines_by_region() |> Cast.cast_title_by_region() @@ -38,7 +37,7 @@ defmodule Matplotex.Figure.CastTest do test "add element for label in axes", %{figure: figure} do assert %Figure{axes: %{element: elements}} = figure - |> Lead.set_regions() + |> Lead.set_regions_areal() |> Cast.cast_label_by_region() assert Enum.any?(elements, fn x -> x.type == "figure.x_label" end) @@ -50,7 +49,7 @@ defmodule Matplotex.Figure.CastTest do test "add element for tick in axes", %{figure: figure} do assert %Figure{axes: %{element: elements, tick: %{x: x_ticks, y: _y_ticks}}} = figure - |> Lead.set_regions() + |> Lead.set_regions_areal() |> Cast.cast_xticks_by_region() assert Enum.filter(elements, fn x -> x.type == "figure.x_tick" end) |> length() == @@ -62,7 +61,7 @@ defmodule Matplotex.Figure.CastTest do test "add element for tick in axes", %{figure: figure} do assert %Figure{axes: %{element: elements, tick: %{y: y_ticks}}} = figure - |> Lead.set_regions() + |> Lead.set_regions_areal() |> Cast.cast_yticks_by_region() assert Enum.filter(elements, fn x -> x.type == "figure.y_tick" end) |> length() == @@ -76,7 +75,7 @@ defmodule Matplotex.Figure.CastTest do test "add elements for horizontal grids", %{figure: figure} do assert %Figure{axes: %{element: elements, tick: %{y: y_ticks}}} = figure - |> Lead.set_regions() + |> Lead.set_regions_areal() |> Cast.cast_yticks_by_region() |> Cast.cast_hgrids_by_region() @@ -89,7 +88,7 @@ defmodule Matplotex.Figure.CastTest do test "add elements for vertical grids", %{figure: figure} do assert %Figure{axes: %{element: elements, tick: %{x: x_ticks}}} = figure - |> Lead.set_regions() + |> Lead.set_regions_areal() |> Cast.cast_xticks_by_region() |> Cast.cast_vgrids_by_region() diff --git a/test/matplotex/figure/lead_test.exs b/test/matplotex/figure/lead_test.exs index 5c1cd93..4f67f80 100644 --- a/test/matplotex/figure/lead_test.exs +++ b/test/matplotex/figure/lead_test.exs @@ -5,9 +5,17 @@ defmodule Matplotex.Figure.LeadTest do use Matplotex.PlotCase alias Matplotex.Figure.Lead - - setup do - figure = Matplotex.FrameHelpers.sample_figure() + setup(context) do + figure = + if Map.get(context, :radial, false) do + categories = ["2008", "2009", "2010", "2011"] + values = [18.48923375, 17.1923791, 17.48479218, 17.02021634] + + colors = ["#76b5c5", "#DEDEDE", "#FBD1A2", "#6195B4"] + Matplotex.pie(values, colors: colors, labels: categories) + else + Matplotex.FrameHelpers.sample_figure() + end frame_width = 8 frame_height = 6 @@ -28,14 +36,14 @@ defmodule Matplotex.Figure.LeadTest do {:ok, %{figure: figure, figure2: figure2}} end - describe "set_regions/1" do + describe "set_regions_areal/1" do test "sets region_xy", %{figure2: figure} do assert %Figure{ axes: %{ region_x: %Region{x: rxx, y: rxy, width: rxwidth, height: rxheight}, region_y: %Region{x: ryx, y: ryy, width: rywidth, height: ryheight} } - } = Lead.set_regions(figure) + } = Lead.set_regions_areal(figure) assert Enum.all?([rxx, rxy, rxwidth, rxheight, ryx, ryy, rywidth, ryheight], &(&1 != 0)) end @@ -45,7 +53,7 @@ defmodule Matplotex.Figure.LeadTest do axes: %{ region_title: %Region{x: rtx, y: rty, width: rtwidth, height: rtheight} } - } = Lead.set_regions(figure) + } = Lead.set_regions_areal(figure) assert Enum.all?([rtx, rty, rtwidth, rtheight], &(&1 != 0)) end @@ -57,7 +65,7 @@ defmodule Matplotex.Figure.LeadTest do axes: %{ region_legend: %Region{x: rlx, y: rly, width: rlwidth, height: rlheight} } - } = Lead.set_regions(figure) + } = Lead.set_regions_areal(figure) assert Enum.all?([rlx, rly, rlwidth, rlheight], &(&1 != 0)) end @@ -69,7 +77,7 @@ defmodule Matplotex.Figure.LeadTest do region_y: %Region{y: ryy, height: ryheight}, region_content: %Region{x: rcx, y: rcy, width: rcwidth, height: rcheight} } - } = Lead.set_regions(figure) + } = Lead.set_regions_areal(figure) assert rxx == rcx assert ryy == rcy @@ -91,7 +99,7 @@ defmodule Matplotex.Figure.LeadTest do tick: %TwoD{x: xticks, y: yticks}, data: {x, y} } - } = Lead.set_regions(figure) + } = Lead.set_regions_areal(figure) assert Enum.min(xticks) == 0 assert Enum.min(yticks) == 0 @@ -112,7 +120,7 @@ defmodule Matplotex.Figure.LeadTest do region_title: %Region{x: rtx, y: rty, width: rtwidth, height: rtheight}, region_content: %Region{x: rcx, y: rcy, width: rcwidth, height: rcheight} } - } = Lead.set_regions(figure) + } = Lead.set_regions_areal(figure) assert rxx == 0 assert ryy == 0 @@ -137,7 +145,7 @@ defmodule Matplotex.Figure.LeadTest do region_title: %Region{height: rtheight}, region_content: %Region{height: rcheight} } - } = Lead.set_regions(figure) + } = Lead.set_regions_areal(figure) margin_two_side = height * margin * 2 assert height == margin_two_side + rxheight + rtheight + rcheight @@ -152,9 +160,90 @@ defmodule Matplotex.Figure.LeadTest do region_content: %Region{width: rcwidth}, region_legend: %Region{width: rlwidth} } - } = Lead.set_regions(figure) + } = Lead.set_regions_areal(figure) two_side_margin = width * margin * 2 assert width == two_side_margin + ry_width + rcwidth + rlwidth end + + describe "set_regions_radial" do + @tag radial: true + test "updates with all borders", %{figure: figure} do + %Figure{axes: %{border: {lx, by, rx, ty}}} = Lead.set_regions_radial(figure) + assert lx != 0 && by != 0 && rx != 0 && ty != 0 + end + + @tag radial: true + test "updates region titles", %{figure: figure} do + %Figure{ + margin: margin, + figsize: {fwidth, fheight}, + axes: %{ + region_title: %Region{ + x: x_region_title, + y: y_region_title, + width: width_region_title, + height: height_region_title + } + } + } = Lead.set_regions_radial(figure) + + width_margin_value = margin * fwidth + height_margin_value = margin * fheight + + assert x_region_title == width_margin_value + assert y_region_title == -(height_margin_value + height_region_title) + assert width_region_title == fwidth - 2 * width_margin_value + end + + @tag radial: true + test "updates region legend", %{figure: figure} do + %Figure{ + margin: margin, + figsize: {fwidth, fheight}, + axes: %{ + region_legend: %Region{ + x: x_region_legend, + y: y_region_legend, + width: width_region_legend + }, + region_title: %Region{height: height_region_title} + } + } = Lead.set_regions_radial(figure) + + width_margin_value = margin * fwidth + height_margin_value = margin * fheight + assert x_region_legend == fwidth - width_margin_value - width_region_legend + + assert abs(y_region_legend) == height_margin_value + height_region_title + end + + @tag radial: true + test "updates region content", %{figure: figure} do + %Figure{ + figsize: {fwidth, fheight}, + axes: %{ + region_content: %Region{ + x: x_region_content, + y: y_region_content, + width: width_region_content, + height: height_region_content + }, + border: {lx, by, _rx, ty}, + region_legend: %Region{width: width_region_legend}, + region_title: %Region{height: height_region_title} + } + } = Lead.set_regions_radial(figure) + + assert x_region_content == lx + assert y_region_content == by + assert 2 * lx + width_region_content + width_region_legend == fwidth + assert 2 * abs(ty) + height_region_content + height_region_title == fheight + end + @tag radial: true + test "sets origin to center of the figure", %{figure: figure} do + assert %Figure{axes: %{center: %TwoD{x: cx, y: cy} }} = Lead.set_regions_radial(figure) + assert cx != 0 && cy != 0 + end + end end diff --git a/test/support/plot_case.ex b/test/support/plot_case.ex index 86f7d65..25bdca5 100644 --- a/test/support/plot_case.ex +++ b/test/support/plot_case.ex @@ -1,5 +1,4 @@ defmodule Matplotex.PlotCase do - alias Matplotex.Figure.Lead use ExUnit.CaseTemplate using do @@ -9,11 +8,11 @@ defmodule Matplotex.PlotCase do end def set_figure() do - figure = Matplotex.FrameHelpers.sample_figure() |> Lead.set_spines() + figure = Matplotex.FrameHelpers.sample_figure() {:ok, %{figure: figure}} end def set_bar() do - {:ok, %{figure: Matplotex.FrameHelpers.bar() |> Lead.set_spines()}} + {:ok, %{figure: Matplotex.FrameHelpers.bar()}} end end