diff --git a/lib/matplotex.ex b/lib/matplotex.ex index 287f306..4ea2146 100644 --- a/lib/matplotex.ex +++ b/lib/matplotex.ex @@ -2,6 +2,7 @@ defmodule Matplotex do @moduledoc """ Module to generate a graph. """ + alias Matplotex.Figure.Areal.Spline alias Matplotex.Figure.Areal.Histogram alias Matplotex.InputError alias Matplotex.Figure.Radial.Pie @@ -33,6 +34,9 @@ defmodule Matplotex do |> BarChart.create({pos, values, width}, opts) end + @doc """ + Creates a scatter plot based on the given data + """ def scatter(stream, opts) when is_struct(stream, Stream) do Scatter.create(stream, opts) end @@ -51,6 +55,9 @@ defmodule Matplotex do |> Scatter.create({x, y}, opts) end + @doc """ + Creates a piec charts based on the size and opts + """ def pie(sizes, opts \\ []) do Pie.create(%Figure{axes: %Pie{}}, sizes, opts) end @@ -84,10 +91,26 @@ defmodule Matplotex do |> LinePlot.create({x, y}, opts) end + @doc """ + Creates a histogram with given data and bins. + + ## Examples + + iex> Matplotex.hist([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 5) + """ + def hist(data, bins), do: hist(data, bins, []) + def hist(data, bins, opts) do Histogram.create(%Figure{axes: %Histogram{}}, {data, bins}, opts) end + def spline(x, y), do: spline(x, y, []) + def spline(x, y, opts), do: spline(%Figure{axes: %Spline{}}, x, y, opts) + + def spline(%Figure{} = figure, x, y, opts) do + Spline.create(figure, {x, y}, opts) + end + @doc """ Sets X and Y labels for the graph with given font details diff --git a/lib/matplotex/element.ex b/lib/matplotex/element.ex index 6d6cf18..cdc4d83 100644 --- a/lib/matplotex/element.ex +++ b/lib/matplotex/element.ex @@ -1,8 +1,11 @@ defmodule Matplotex.Element do @callback assemble(element :: struct()) :: String.t() - @callback flipy(element :: struct(), height :: number()) :: struct() def assemble(%module{} = element), do: module.assemble(element) + def to_pixel({x, y}) do + "#{to_pixel(x)},#{to_pixel(y)}" + end + def to_pixel(inch) when is_number(inch), do: inch * 96 def to_pixel(_), do: 0 diff --git a/lib/matplotex/element/circle.ex b/lib/matplotex/element/circle.ex index 6ee9682..79601c7 100644 --- a/lib/matplotex/element/circle.ex +++ b/lib/matplotex/element/circle.ex @@ -32,8 +32,4 @@ defmodule Matplotex.Element.Circle do def get_cx(%{cx: x}), do: to_pixel(x) def get_cy(%{cy: y}), do: to_pixel(y) def get_r(%{r: r}), do: to_pixel(r) - @impl Element - def flipy(%__MODULE__{cy: y} = circle, height) do - %__MODULE__{circle | cy: height - y} - end end diff --git a/lib/matplotex/element/label.ex b/lib/matplotex/element/label.ex index c2c1aef..bc29554 100644 --- a/lib/matplotex/element/label.ex +++ b/lib/matplotex/element/label.ex @@ -71,8 +71,4 @@ defmodule Matplotex.Element.Label do def get_y(%{y: y}), do: to_pixel(y) def get_width(%{width: width}), do: to_pixel(width) def get_height(%{height: height}), do: to_pixel(height) - @impl Element - def flipy(%__MODULE__{y: y} = label, height) do - %__MODULE__{label | y: height - y} - end end diff --git a/lib/matplotex/element/legend.ex b/lib/matplotex/element/legend.ex index dcddf93..340e44c 100644 --- a/lib/matplotex/element/legend.ex +++ b/lib/matplotex/element/legend.ex @@ -30,11 +30,6 @@ defmodule Matplotex.Element.Legend do """ end - @impl Element - def flipy(%__MODULE__{y: y} = legend, height) do - %__MODULE__{legend | y: height - y, label: Label.flipy(legend.label, height)} - end - defp handle(%__MODULE__{handle: %handle_element{} = handle}) do handle_element.assemble(handle) end diff --git a/lib/matplotex/element/line.ex b/lib/matplotex/element/line.ex index 173c15c..4f6d914 100644 --- a/lib/matplotex/element/line.ex +++ b/lib/matplotex/element/line.ex @@ -52,10 +52,6 @@ defmodule Matplotex.Element.Line do def get_x2(%{x2: x2}), do: to_pixel(x2) def get_y1(%{y1: y1}), do: to_pixel(y1) def get_y2(%{y2: y2}), do: to_pixel(y2) - @impl Element - def flipy(%__MODULE__{y1: y1, y2: y2} = line, height) do - %__MODULE__{line | y1: height - y1, y2: height - y2} - end defp stroke_dasharray(%{linestyle: "_"}), do: nil diff --git a/lib/matplotex/element/polygon.ex b/lib/matplotex/element/polygon.ex index c415c94..47747a0 100644 --- a/lib/matplotex/element/polygon.ex +++ b/lib/matplotex/element/polygon.ex @@ -15,18 +15,6 @@ defmodule Matplotex.Element.Polygon do ) end - @impl Element - def flipy(%__MODULE__{points: point} = label, height) do - %__MODULE__{label | points: flip_point(point, height)} - end - - defp flip_point(point, height) do - Enum.map(point, &flip_coord(&1, height)) - end - - defp flip_coord({x, y}, height) do - {x, height - y} - end defp assemble_point(%{points: point}) do for {x, y} <- point do diff --git a/lib/matplotex/element/rad_legend.ex b/lib/matplotex/element/rad_legend.ex index cf3c87b..7ff0294 100644 --- a/lib/matplotex/element/rad_legend.ex +++ b/lib/matplotex/element/rad_legend.ex @@ -30,11 +30,6 @@ defmodule Matplotex.Element.RadLegend do """ end - @impl Element - def flipy(%__MODULE__{y: y} = legend, height) do - %__MODULE__{legend | y: height - y, label: Label.flipy(legend.label, height)} - end - def with_label( %__MODULE__{ label: text, diff --git a/lib/matplotex/element/rect.ex b/lib/matplotex/element/rect.ex index c727a99..71591c2 100644 --- a/lib/matplotex/element/rect.ex +++ b/lib/matplotex/element/rect.ex @@ -50,8 +50,4 @@ defmodule Matplotex.Element.Rect do def get_y(%{y: y}), do: to_pixel(y) def get_width(%{width: width}), do: to_pixel(width) def get_height(%{height: height}), do: to_pixel(height) - @impl Element - def flipy(%__MODULE__{y: y} = rect, height) do - %__MODULE__{rect | y: height - y} - end end diff --git a/lib/matplotex/element/slice.ex b/lib/matplotex/element/slice.ex index 5891ef8..7c1a436 100644 --- a/lib/matplotex/element/slice.ex +++ b/lib/matplotex/element/slice.ex @@ -37,8 +37,4 @@ defmodule Matplotex.Element.Slice do def get_cx(%{cx: cx}), do: to_pixel(cx) def get_cy(%{cy: cy}), do: to_pixel(cy) def get_radius(%{radius: radius}), do: to_pixel(radius) - @impl Element - def flipy(slice, _height) do - slice - end end diff --git a/lib/matplotex/element/spline.ex b/lib/matplotex/element/spline.ex new file mode 100644 index 0000000..a3f60c6 --- /dev/null +++ b/lib/matplotex/element/spline.ex @@ -0,0 +1,40 @@ +defmodule Matplotex.Element.Spline do + alias Matplotex.Element + use Element + @default_stroke_width 2 + @default_stroke "black" + + @fill "none" + defstruct [ + :type, + :moveto, + :cubic, + :smooths, + fill: @fill, + stroke: @default_stroke, + stroke_width: @default_stroke_width + ] + + @impl Element + def assemble(element) do + """ + + """ + end + + defp points(points) do + for point <- points do + "#{to_pixel(point)} " + end + end + + defp smooth_beizer(points) do + for point <- points do + "S #{points(point)}" + end + end +end diff --git a/lib/matplotex/element/tick.ex b/lib/matplotex/element/tick.ex index 8b7b41e..711d8d8 100644 --- a/lib/matplotex/element/tick.ex +++ b/lib/matplotex/element/tick.ex @@ -17,13 +17,4 @@ defmodule Matplotex.Element.Tick do #{Element.assemble(tick.label)} ) end - - @impl Element - def flipy(%__MODULE__{label: label, tick_line: tick_line} = tick, height) do - %__MODULE__{ - tick - | label: Label.flipy(label, height), - tick_line: Line.flipy(tick_line, height) - } - end end diff --git a/lib/matplotex/figure/areal.ex b/lib/matplotex/figure/areal.ex index adf0ce0..b45910f 100644 --- a/lib/matplotex/figure/areal.ex +++ b/lib/matplotex/figure/areal.ex @@ -80,13 +80,18 @@ defmodule Matplotex.Figure.Areal do |> update_tick(tick) end - def add_ticks(%__MODULE__{tick: tick, size: {width, height}= size} = axes, {key, {_min, _max} = lim}) do - number_of_ticks = if key == :x do - ceil(width) - else - ceil(height) - end - {ticks, lim} = Ticker.generate_ticks(lim, number_of_ticks ) + def add_ticks( + %__MODULE__{tick: tick, size: {width, height} = size} = axes, + {key, {_min, _max} = lim} + ) do + number_of_ticks = + if key == :x do + ceil(width) + else + ceil(height) + end + + {ticks, lim} = Ticker.generate_ticks(lim, number_of_ticks) tick = Map.put(tick, key, ticks) @@ -122,7 +127,6 @@ defmodule Matplotex.Figure.Areal do %{axes | legend: legend} end - defp update_limit(%TwoD{x: nil} = limit, :x, xlim) do %TwoD{limit | x: xlim} end diff --git a/lib/matplotex/figure/areal/histogram.ex b/lib/matplotex/figure/areal/histogram.ex index 64c342e..ce60acc 100644 --- a/lib/matplotex/figure/areal/histogram.ex +++ b/lib/matplotex/figure/areal/histogram.ex @@ -23,11 +23,20 @@ defmodule Matplotex.Figure.Areal.Histogram do ) @impl Areal - def create(%Figure{axes: %__MODULE__{} = axes, rc_params: rc_params} = figure, {data, bins}, opts) do + def create( + %Figure{axes: %__MODULE__{} = axes, rc_params: rc_params} = figure, + {data, bins}, + opts + ) do {x, y} = bins_and_hists(data, bins) dataset = Dataset.cast(%Dataset{x: x, y: y}, opts) - %Figure{figure | axes: %__MODULE__{axes | data: {x, y}, dataset: [dataset]}, rc_params: %RcParams{rc_params | y_padding: @make_it_zero}} + + %Figure{ + figure + | axes: %__MODULE__{axes | data: {x, y}, dataset: [dataset]}, + rc_params: %RcParams{rc_params | y_padding: @make_it_zero} + } |> PlotOptions.set_options_in_figure(opts) end @@ -39,18 +48,38 @@ defmodule Matplotex.Figure.Areal.Histogram do |> materialize_hist() end - defp materialize_hist(%Figure{axes: %{dataset: data,limit: %TwoD{x: x_lim, y: y_lim}, region_content: %Region{x: x_region_content, y: y_region_content, width: width_region_content, height: height_region_content}, element: element}, rc_params: %RcParams{x_padding: x_padding, white_space: white_space}}) do + defp materialize_hist(%Figure{ + axes: %{ + dataset: data, + limit: %TwoD{x: x_lim, y: y_lim}, + region_content: %Region{ + x: x_region_content, + y: y_region_content, + width: width_region_content, + height: height_region_content + }, + element: element + }, + rc_params: %RcParams{x_padding: x_padding, white_space: white_space} + }) do x_padding_value = width_region_content * x_padding + white_space shrinked_width_region_content = width_region_content - x_padding_value * 2 hist_elements = - data - |>Enum.map(fn dataset -> - dataset - |> do_transform(x_lim, y_lim, shrinked_width_region_content, height_region_content, {x_region_content + x_padding_value, y_region_content}) - |> capture(abs(y_region_content), shrinked_width_region_content) - end) - |>List.flatten() + data + |> Enum.map(fn dataset -> + dataset + |> do_transform( + x_lim, + y_lim, + shrinked_width_region_content, + height_region_content, + {x_region_content + x_padding_value, y_region_content} + ) + |> capture(abs(y_region_content), shrinked_width_region_content) + end) + |> List.flatten() + %Figure{axes: %{element: element ++ hist_elements}} end @@ -95,12 +124,12 @@ defmodule Matplotex.Figure.Areal.Histogram do defp capture([], captured, _dataset, _bly, _region_width), do: captured - defp bin_position(x, pos_factor) when pos_factor < 0 do x + pos_factor end defp bin_position(x, _pos_factor), do: x + defp bins_and_hists(data, bins) do {data_min, data_max} = Enum.min_max(data) @@ -118,10 +147,16 @@ defmodule Matplotex.Figure.Areal.Histogram do {bins_dist, hists} end - defp sanitize(%Figure{axes: %__MODULE__{data: {x, y}}= axes} = figure) do + defp sanitize(%Figure{axes: %__MODULE__{data: {x, y}} = axes} = figure) do {ymin, ymax} = Enum.min_max(y) {xmin, xmax} = Enum.min_max(x) - %Figure{figure | axes: %__MODULE__{axes | limit: %TwoD{x: {floor(xmin), ceil(xmax)},y: {floor(ymin), ceil(ymax)}}}} - end + %Figure{ + figure + | axes: %__MODULE__{ + axes + | limit: %TwoD{x: {floor(xmin), ceil(xmax)}, y: {floor(ymin), ceil(ymax)}} + } + } + end end diff --git a/lib/matplotex/figure/areal/spline.ex b/lib/matplotex/figure/areal/spline.ex new file mode 100644 index 0000000..30ea4a5 --- /dev/null +++ b/lib/matplotex/figure/areal/spline.ex @@ -0,0 +1,101 @@ +defmodule Matplotex.Figure.Areal.Spline do + alias Matplotex.Element.Spline + alias Matplotex.Figure.RcParams + alias Matplotex.Figure.Areal + alias Matplotex.Figure.Dataset + alias Matplotex.Figure.Areal.PlotOptions + alias Matplotex.Figure.Areal.Region + alias Matplotex.Figure.TwoD + alias Matplotex.Figure + + + use Areal + + frame( + tick: %TwoD{}, + limit: %TwoD{}, + label: %TwoD{}, + region_x: %Region{}, + region_y: %Region{}, + region_title: %Region{}, + region_legend: %Region{}, + region_content: %Region{} + ) + @impl Areal + def create( + %Figure{axes: %__MODULE__{dataset: data} = axes} = figure, + {x, y}, + opts + ) do + x = determine_numeric_value(x) + y = determine_numeric_value(y) + opts = Keyword.put_new(opts, :color, "none") + dataset = Dataset.cast(%Dataset{x: x, y: y}, opts) + datasets = data ++ [dataset] + xydata = flatten_for_data(datasets) + + %Figure{figure | axes: %{axes | data: xydata, dataset: datasets}} + |> PlotOptions.set_options_in_figure(opts) + end + + @impl Areal + def materialize(figure) do + figure + |>__MODULE__.materialized_by_region() + |> materialize_spline() + + end + defp materialize_spline(%Figure{axes: + %{ + dataset: data, + limit: %{x: xlim, y: ylim}, + region_content: %Region{ + x: x_region_content, + y: y_region_content, + width: width_region_content, + height: height_region_content + }, + element: elements + } = axes, + rc_params: %RcParams{x_padding: x_padding, y_padding: y_padding}} = figure) do + x_padding_value = width_region_content * x_padding + y_padding_value = height_region_content * y_padding + shrinked_width_region_content = width_region_content - x_padding_value * 2 + shrinked_height_region_content = height_region_content - y_padding_value * 2 + transition = {x_region_content + x_padding_value, y_region_content + y_padding_value} + line_elements = + data + |> Enum.map(fn dataset -> + dataset + |> do_transform( + xlim, + ylim, + shrinked_width_region_content, + shrinked_height_region_content, + transition + ) + |> capture(transition) + end) + |> List.flatten() + + elements = elements ++ line_elements + + %Figure{figure | axes: %{axes | element: elements}} + end + + + + defp capture(%Dataset{transformed: transformed, color: color,edge_color: edge_color, line_width: stroke_width}, move_to_def) do + {moveto, transformed} = List.pop_at(transformed, 0, move_to_def) + cubic = Enum.slice(transformed, 0..2) + smooths = blend(transformed, 3) + %Spline{type: "figure.spline", moveto: moveto, cubic: cubic, smooths: smooths, fill: color, stroke: edge_color, stroke_width: stroke_width} + end + + defp blend(smooths, start_from) do + smooths + |>Enum.slice(start_from..-1//1) + |>Enum.chunk_every(2) + + end +end diff --git a/lib/matplotex/figure/areal/ticker.ex b/lib/matplotex/figure/areal/ticker.ex index 34bd420..a13192c 100644 --- a/lib/matplotex/figure/areal/ticker.ex +++ b/lib/matplotex/figure/areal/ticker.ex @@ -4,6 +4,7 @@ defmodule Matplotex.Figure.Areal.Ticker do step = (max - min) / @tick_in_plot produce_ticks(min, max, step, [format_number(min)]) end + def generate_ticks({lower_limit, upper_limit} = lim, number_of_ticks) do {lower_limit |> Nx.linspace(upper_limit, n: number_of_ticks) |> Nx.to_list(), lim} end diff --git a/lib/matplotex/figure/cast.ex b/lib/matplotex/figure/cast.ex index 9760562..7d5c473 100644 --- a/lib/matplotex/figure/cast.ex +++ b/lib/matplotex/figure/cast.ex @@ -325,7 +325,6 @@ defmodule Matplotex.Figure.Cast do %Figure{figure | axes: %{axes | element: element}} end - def cast_xticks_by_region( %Figure{ axes: @@ -427,7 +426,6 @@ defmodule Matplotex.Figure.Cast do @lowest_tick |> Nx.linspace(axis_size, n: number_of_ticks_required) |> Nx.to_list() end - def cast_yticks_by_region( %Figure{ axes: @@ -693,7 +691,6 @@ defmodule Matplotex.Figure.Cast do def cast_legends(figure), do: figure - defp calculate_center(%Coords{bottom_left: bottom_left, bottom_right: bottom_right}, {x, y}, :x) do {calculate_distance(bottom_left, bottom_right) / 2 + x, y} end @@ -722,7 +719,6 @@ defmodule Matplotex.Figure.Cast do defp rotate_label(:y), do: -90 - defp confine_ticks([{_l, _v} | _] = ticks, {min, max}) do ticks |> Enum.filter(fn {_l, tick} -> diff --git a/lib/matplotex/figure/dataset.ex b/lib/matplotex/figure/dataset.ex index a1cb0ea..26d53f3 100644 --- a/lib/matplotex/figure/dataset.ex +++ b/lib/matplotex/figure/dataset.ex @@ -6,6 +6,7 @@ defmodule Matplotex.Figure.Dataset do @default_marker_size 3.5 @default_stroke "black" @default_alpha 1.0 + @line_width 2 defstruct [ :label, @@ -19,7 +20,8 @@ defmodule Matplotex.Figure.Dataset do alpha: @default_alpha, marker: @default_marker, linestyle: @default_linestyle, - marker_size: @default_marker_size + marker_size: @default_marker_size, + line_width: @line_width ] def cast(dataset, values) do diff --git a/lib/matplotex/helpers.ex b/lib/matplotex/helpers.ex index 61284bc..cc9fee8 100644 --- a/lib/matplotex/helpers.ex +++ b/lib/matplotex/helpers.ex @@ -312,8 +312,28 @@ defmodule Matplotex.Helpers do values = Nx.Random.key(12) |> Nx.Random.normal(0, 1, shape: {1000}) |> elem(0) |> Nx.to_list() bins = 30 - Matplotex.hist(values, bins, x_label: "Value", y_label: "Frequency", title: "Histogram", color: "blue", edge_color: "black", alpha: 0.7, x_ticks_count: 9) + Matplotex.hist(values, bins, + x_label: "Value", + y_label: "Frequency", + title: "Histogram", + color: "blue", + edge_color: "black", + alpha: 0.7, + x_ticks_count: 9 + ) + |> Matplotex.show() + |> copy() + end + + def spline() do + x_nx = Nx.linspace(0, 10, n: 100) + x = Nx.to_list(x_nx) + y = x_nx |> Nx.sin() |> Nx.to_list() + + + Matplotex.spline(x, y, x_label: "X", y_label: "Y", edge_color: "green") |> Matplotex.show() |> copy() end + end diff --git a/test/matplotex/figure/areal/histogram_test.exs b/test/matplotex/figure/areal/histogram_test.exs index 5198851..8c3f978 100644 --- a/test/matplotex/figure/areal/histogram_test.exs +++ b/test/matplotex/figure/areal/histogram_test.exs @@ -24,7 +24,7 @@ defmodule Matplotex.Figure.Areal.HistogramTest do assert y_label.text == "Frequency" x_ticks = elements |> Enum.filter(fn elem -> elem.type == "figure.x_tick" end) y_ticks = elements |> Enum.filter(fn elem -> elem.type == "figure.y_tick" end) - histogram_elements = elements|>Enum.filter(fn elem -> elem.type == "figure.histogram" end) + histogram_elements = elements |> Enum.filter(fn elem -> elem.type == "figure.histogram" end) assert length(histogram_elements) == bins assert length(x_ticks) > 3 assert length(y_ticks) > 3 diff --git a/test/matplotex/figure/areal/spline_test.exs b/test/matplotex/figure/areal/spline_test.exs new file mode 100644 index 0000000..c7cd645 --- /dev/null +++ b/test/matplotex/figure/areal/spline_test.exs @@ -0,0 +1,19 @@ +defmodule Matplotex.Figure.Areal.SplineTest do + use Matplotex.PlotCase + alias Matplotex.Figure + alias Matplotex.Figure.Areal.Spline + setup do + x_nx = Nx.linspace(0, 10, n: 100) + x = Nx.to_list(x_nx) + y = x_nx |> Nx.sin() |> Nx.to_list() + + figure = Matplotex.spline(x, y, x_label: "X", y_label: "Y") + {:ok, %{figure: figure}} + end + + + test "adds a spline element in a figure",%{figure: figure} do + assert %Figure{axes: %Spline{element: elements}} =Spline.materialize(figure) + assert Enum.any?(elements, &(&1.type=="figure.spline")) + end +end diff --git a/test/matplotex_test.exs b/test/matplotex_test.exs index a30a611..5cd749d 100644 --- a/test/matplotex_test.exs +++ b/test/matplotex_test.exs @@ -288,4 +288,20 @@ defmodule MatplotexTest do assert figure.axes.label.y == "Frequency" end end + + describe "spline" do + test "creates a figure for spline" do + x_nx = Nx.linspace(0, 10, n: 100) + x = Nx.to_list(x_nx) + y = x_nx |> Nx.sin() |> Nx.to_list() + + assert %Figure{axes: %{dataset: [data1], label: label}} = + Matplotex.spline(x, y, x_label: "X", y_label: "Y") + + assert label.x == "X" + assert label.y == "Y" + assert data1.x == x + assert data1.y == y + end + end end