diff --git a/lib/matplotex.ex b/lib/matplotex.ex index 3720f77..0f7c402 100644 --- a/lib/matplotex.ex +++ b/lib/matplotex.ex @@ -213,10 +213,6 @@ defmodule Matplotex do bar(pos, values, width, []) end - def bar(pos, values, width, opts) do - BarChart.create(%Figure{axes: %BarChart{}}, {pos, values, width}, opts) - end - @doc """ Adds an additional dataset to a bar plot in the given `%Figure{}`. @@ -251,6 +247,13 @@ defmodule Matplotex do |> M.bar(0, values2, width, label: "Dataset2", color: "#D3D3D3") |> M.bar(width, values2, width, label: "Dataset3", color: "green") """ + + def bar(%Figure{} = figure, values, width, opts), do: bar(figure, width, values, width, opts) + + def bar(pos, values, width, opts) do + BarChart.create(%Figure{axes: %BarChart{}}, {pos, values, width}, opts) + end + def bar(%Figure{} = figure, pos, values, width, opts) do figure |> show_legend() @@ -752,6 +755,10 @@ defmodule Matplotex do Figure.show_legend(figure) end + def hide_legend(figure) do + Figure.hide_legend(figure) + end + def set_options(figure, opts) do PlotOptions.set_options_in_figure(figure, opts) end diff --git a/lib/matplotex/colorscheme/blender.ex b/lib/matplotex/colorscheme/blender.ex index 460d32f..9cc549b 100644 --- a/lib/matplotex/colorscheme/blender.ex +++ b/lib/matplotex/colorscheme/blender.ex @@ -1,19 +1,17 @@ defmodule Matplotex.Colorscheme.Blender do -@moduledoc false -alias Matplotex.Colorscheme.Rgb + @moduledoc false + alias Matplotex.Colorscheme.Rgb @rgb_fields [:red, :green, :blue, :alpha] @type color :: %Rgb{ - red: :float, - green: :float, - blue: :float, - alpha: :float - } - + red: :float, + green: :float, + blue: :float, + alpha: :float + } def mix(color1, color2, weight \\ 0.5) do - p = weight w = p * 2 - 1 a = color1.alpha - color2.alpha @@ -30,10 +28,10 @@ alias Matplotex.Colorscheme.Rgb alpha = get_alpha(color1) * p + get_alpha(color2) * (1 - p) rgb(r, g, b, alpha) end + defdelegate rgb(red, green, blue), to: Rgb defdelegate rgb(red, green, blue, alpha), to: Rgb - @doc """ Gets the `:red` property of the color. """ @@ -87,6 +85,4 @@ alias Matplotex.Colorscheme.Rgb end defp cast_color_by_attribute(color, attribute) when attribute in @rgb_fields, do: color - - end diff --git a/lib/matplotex/colorscheme/colormap.ex b/lib/matplotex/colorscheme/colormap.ex index 1a8baee..90f52c2 100644 --- a/lib/matplotex/colorscheme/colormap.ex +++ b/lib/matplotex/colorscheme/colormap.ex @@ -1,20 +1,21 @@ defmodule Matplotex.Colorscheme.Colormap do -@moduledoc false + @moduledoc false defstruct [:color, :offset, opacity: 1] + def viridis do - ["#fde725","#21918c","#3b528b","#440154"] + ["#fde725", "#21918c", "#3b528b", "#440154"] end def plasma do - ["#F7E425","#ED6925", "#9C179E", "#0C0786" ] + ["#F7E425", "#ED6925", "#9C179E", "#0C0786"] end def inferno do - ["#FCFFA4","#F56C3E","#B12A90","#000004"] + ["#FCFFA4", "#F56C3E", "#B12A90", "#000004"] end def magma do - ["#FCFDBF","#FB8861", "#B73779", "#000004"] + ["#FCFDBF", "#FB8861", "#B73779", "#000004"] end def fetch_cmap(cmap) when is_binary(cmap), do: cmap |> String.to_atom() |> fetch_cmap() @@ -22,8 +23,10 @@ defmodule Matplotex.Colorscheme.Colormap do def fetch_cmap(cmap) do apply(__MODULE__, cmap, []) |> make_colormap() end + def make_colormap(colors) do size = length(colors) + colors |> Enum.with_index() |> Enum.map(&colormap(&1, size)) @@ -35,6 +38,4 @@ defmodule Matplotex.Colorscheme.Colormap do offset = idx / size * 100 %__MODULE__{color: color, offset: offset} end - - end diff --git a/lib/matplotex/colorscheme/garner.ex b/lib/matplotex/colorscheme/garner.ex index d3e279a..fa3e08b 100644 --- a/lib/matplotex/colorscheme/garner.ex +++ b/lib/matplotex/colorscheme/garner.ex @@ -1,13 +1,14 @@ defmodule Matplotex.Colorscheme.Garner do -@moduledoc false -alias Matplotex.Colorscheme.Rgb -alias Matplotex.Colorscheme.Blender -alias Matplotex.InputError + @moduledoc false + alias Matplotex.Colorscheme.Rgb + alias Matplotex.Colorscheme.Blender + alias Matplotex.InputError - defstruct [:range, :color_cue, :cmap, :preceeding, :minor, :major, :final] + defstruct [:range, :color_cue, :cmap, :preceeding, :minor, :major, :final] def garn_color({min, max} = range, point, cmap) when max != min do cue = (point - min) / (max - min) + cmap |> make_from_cmap() |> put_range(range, cue) @@ -16,8 +17,8 @@ alias Matplotex.InputError defp make_from_cmap(cmap) do cmap - |>to_rgb() - |>place_edges() + |> to_rgb() + |> place_edges() end defp put_range(%__MODULE__{} = garner, range, cue) do @@ -28,22 +29,29 @@ alias Matplotex.InputError Enum.map(color_map, &Rgb.from_cmap!(&1)) end - defp place_edges([preceeding, minor, major,final]) do - %__MODULE__{preceeding: preceeding.color, minor: minor.color, major: major.color, final: final.color} + defp place_edges([preceeding, minor, major, final]) do + %__MODULE__{ + preceeding: preceeding.color, + minor: minor.color, + major: major.color, + final: final.color + } end + defp place_edges(_) do raise InputError, message: "Invalid colormap" end - defp point_color(%__MODULE__{color_cue: cue, preceeding: preceeding, minor: minor}) when cue < minor do - minor|> Blender.mix(preceeding, cue)|> Rgb.to_string() + defp point_color(%__MODULE__{color_cue: cue, preceeding: preceeding, minor: minor}) + when cue < minor do + minor |> Blender.mix(preceeding, cue) |> Rgb.to_string() end defp point_color(%__MODULE__{color_cue: cue, minor: minor, major: major}) when cue < major do - major|> Blender.mix(minor, cue)|> Rgb.to_string() + major |> Blender.mix(minor, cue) |> Rgb.to_string() end defp point_color(%__MODULE__{color_cue: cue, major: major, final: final}) when cue >= major do - final|> Blender.mix(major)|> Rgb.to_string() + final |> Blender.mix(major) |> Rgb.to_string() end - end +end diff --git a/lib/matplotex/colorscheme/rgb.ex b/lib/matplotex/colorscheme/rgb.ex index 590c960..3c350ce 100644 --- a/lib/matplotex/colorscheme/rgb.ex +++ b/lib/matplotex/colorscheme/rgb.ex @@ -1,18 +1,25 @@ defmodule Matplotex.Colorscheme.Rgb do @moduledoc false -alias Matplotex.Colorscheme.Colormap + alias Matplotex.Colorscheme.Colormap + defstruct [ - red: 0.0, # 0-255 - green: 0.0, # 0-255 - blue: 0.0, # 0-255 - alpha: 1.0 # 0-1 + # 0-255 + red: 0.0, + # 0-255 + green: 0.0, + # 0-255 + blue: 0.0, + # 0-1 + alpha: 1.0 ] - def rgb(red, green, blue, alpha\\1.0) - def rgb({red, :percent}, {green, :percent}, {blue, :percent}, alpha) do + def rgb(red, green, blue, alpha \\ 1.0) + + def rgb({red, :percent}, {green, :percent}, {blue, :percent}, alpha) do rgb(red * 255, green * 255, blue * 255, alpha) end - def rgb(red, green, blue, alpha) do + + def rgb(red, green, blue, alpha) do %__MODULE__{ red: cast(red, :red), green: cast(green, :green), @@ -21,65 +28,64 @@ alias Matplotex.Colorscheme.Colormap } end - def to_string(struct, type\\nil) + def to_string(struct, type \\ nil) + + def to_string(struct, nil) do + type = + case struct.alpha do + 1.0 -> :hex + _ -> :rgba + end - def to_string(struct, :nil) do - type = case struct.alpha do - 1.0 -> :hex - _ -> :rgba - end to_string(struct, type) end def to_string(%__MODULE__{red: r, green: g, blue: b, alpha: alpha}, :rgba) do "rgba(#{round(r)}, #{round(g)}, #{round(b)}, #{alpha})" end + def to_string(%__MODULE__{red: r, green: g, blue: b, alpha: 1.0}, :hex) do "#" <> to_hex(r) <> to_hex(g) <> to_hex(b) end def cast(value, field) when field in [:red, :green, :blue] do - value/1 + (value / 1) |> min(255.0) |> max(0.0) end + def cast(value, :alpha) do - value/1 + (value / 1) |> min(1.0) |> max(0.0) end - defp to_hex(value) when is_float(value), do: - to_hex(round(value)) - defp to_hex(value) when value < 16, do: - "0" <> Integer.to_string(value, 16) - defp to_hex(value) when is_integer(value), do: - Integer.to_string(value, 16) - - def from_hex!(input) do - {:ok, color} = from_hex(input) - color - end + defp to_hex(value) when is_float(value), do: to_hex(round(value)) + defp to_hex(value) when value < 16, do: "0" <> Integer.to_string(value, 16) + defp to_hex(value) when is_integer(value), do: Integer.to_string(value, 16) - def from_cmap!(%Colormap{color: color} = cmap) do - %Colormap{cmap | color: from_hex!(color) } - end + def from_hex!(input) do + {:ok, color} = from_hex(input) + color + end - def from_hex("#" <> <>) do - {:ok, rgb(parse_hex(r), parse_hex(g), parse_hex(b))} - end - def from_hex("#" <> <>) do - {:ok, rgb(parse_hex(r <> r), parse_hex(g <> g), parse_hex(b <> b))} - end + def from_cmap!(%Colormap{color: color} = cmap) do + %Colormap{cmap | color: from_hex!(color)} + end - defp parse_hex(s), do: String.to_integer(s, 16) + def from_hex("#" <> <>) do + {:ok, rgb(parse_hex(r), parse_hex(g), parse_hex(b))} + end + def from_hex("#" <> <>) do + {:ok, rgb(parse_hex(r <> r), parse_hex(g <> g), parse_hex(b <> b))} + end + defp parse_hex(s), do: String.to_integer(s, 16) end - defimpl String.Chars, for: CssColors.RGB do -def to_string(struct) do - Matplotex.Colorscheme.Rgb.to_string(struct) -end + def to_string(struct) do + Matplotex.Colorscheme.Rgb.to_string(struct) + end end diff --git a/lib/matplotex/element.ex b/lib/matplotex/element.ex index 2a80b6d..20e7c8d 100644 --- a/lib/matplotex/element.ex +++ b/lib/matplotex/element.ex @@ -17,7 +17,7 @@ defmodule Matplotex.Element do defimpl String.Chars, for: __MODULE__ do def to_string(%module{} = element) do - module.assemble(element) + module.assemble(element) end end end diff --git a/lib/matplotex/element/cmap.ex b/lib/matplotex/element/cmap.ex index 0494e1a..ac71971 100644 --- a/lib/matplotex/element/cmap.ex +++ b/lib/matplotex/element/cmap.ex @@ -1,5 +1,5 @@ defmodule Matplotex.Element.Cmap do -@moduledoc false + @moduledoc false alias Matplotex.Element.Rect alias Matplotex.Element use Element diff --git a/lib/matplotex/figure.ex b/lib/matplotex/figure.ex index 6ac9857..bdaa0c5 100644 --- a/lib/matplotex/figure.ex +++ b/lib/matplotex/figure.ex @@ -63,6 +63,9 @@ defmodule Matplotex.Figure do def show_legend(%__MODULE__{axes: %module{} = axes} = figure), do: %{figure | axes: module.show_legend(axes)} + def hide_legend(%__MODULE__{axes: %module{} = axes} = figure), + do: %{figure | axes: module.hide_legend(axes)} + def set_figure_size(%__MODULE__{margin: margin, axes: axes} = figure, {fwidth, fheight} = fsize) do frame_size = {fwidth - fwidth * 2 * margin, fheight - fheight * 2 * margin} diff --git a/lib/matplotex/figure/areal.ex b/lib/matplotex/figure/areal.ex index 416c9d1..9c23061 100644 --- a/lib/matplotex/figure/areal.ex +++ b/lib/matplotex/figure/areal.ex @@ -58,7 +58,6 @@ defmodule Matplotex.Figure.Areal do end def add_title(axes, title, opts) when is_binary(title) do - # title = Text.create_text(title, opts) %{axes | title: title, show_title: true} end @@ -171,6 +170,20 @@ defmodule Matplotex.Figure.Areal do end end + # For stacked bar chart the flattening supposed to be the sumation of yaxis data + def flatten_for_data(datasets, nil), do: flatten_for_data(datasets) + + def flatten_for_data([%{x: x, y: y} | _datasets], bottom) do + y = + bottom + |> Kernel.++([y]) + |> Nx.tensor(names: [:x, :y]) + |> Nx.sum(axes: [:x]) + |> Nx.to_list() + + {x, y} + end + def flatten_for_data(datasets) do datasets |> Enum.map(fn %{x: x, y: y} -> {x, y} end) @@ -206,6 +219,10 @@ defmodule Matplotex.Figure.Areal do %__MODULE__{axes | show_legend: true} end + def hide_legend(%__MODULE__{} = axes) do + %__MODULE__{axes | show_legend: false} + end + def set_frame_size(%__MODULE__{} = axes, frame_size) do %__MODULE__{axes | size: frame_size} end @@ -382,6 +399,27 @@ defmodule Matplotex.Figure.Areal do Algebra.transform_given_point(x, y, sx, sy, tx, ty) end + def do_transform( + %Dataset{x: x, y: y, bottom: bottom} = dataset, + xlim, + ylim, + width, + height, + transition + ) + when is_list(bottom) do + y = [y | bottom] |> Nx.tensor() |> Nx.transpose() |> Nx.to_list() + + transformed = + x + |> Enum.zip(y) + |> Enum.map(fn {x, y} -> + transform_with_bottom(x, y, xlim, ylim, width, height, transition) + end) + + %Dataset{dataset | transformed: transformed} + end + def do_transform(%Dataset{x: x, y: y} = dataset, xlim, ylim, width, height, transition) do transformed = x @@ -394,4 +432,19 @@ defmodule Matplotex.Figure.Areal do %Dataset{dataset | transformed: transformed} end + + defp transform_with_bottom(x, y, xlim, ylim, width, height, transition) when is_list(y) do + y_top = Enum.sum(y) + y_bottom = y |> tl() |> Enum.sum() + + transformed = + transformation(x, y_top, xlim, ylim, width, height, transition) + |> Algebra.flip_y_coordinate() + + {_, transformed_y_bottom} = + transformation(x, y_bottom, xlim, ylim, width, height, transition) + |> Algebra.flip_y_coordinate() + + {transformed, transformed_y_bottom} + end end diff --git a/lib/matplotex/figure/areal/bar_chart.ex b/lib/matplotex/figure/areal/bar_chart.ex index d87734e..16ae106 100644 --- a/lib/matplotex/figure/areal/bar_chart.ex +++ b/lib/matplotex/figure/areal/bar_chart.ex @@ -36,7 +36,8 @@ defmodule Matplotex.Figure.Areal.BarChart do x = hypox(values) dataset = Dataset.cast(%Dataset{x: x, y: values, pos: pos, width: width}, opts) datasets = data ++ [dataset] - xydata = flatten_for_data(datasets) + bottom = Keyword.get(opts, :bottom) + xydata = datasets |> Enum.reverse() |> flatten_for_data(bottom) %Figure{ figure @@ -126,6 +127,41 @@ defmodule Matplotex.Figure.Areal.BarChart do end end + def capture( + [{{x, y}, bottom} | to_capture], + captured, + %Dataset{ + color: color, + width: width, + pos: pos_factor, + edge_color: edge_color, + alpha: alpha, + line_width: line_width + } = dataset, + bly + ) do + capture( + to_capture, + captured ++ + [ + %Rect{ + type: "figure.bar", + x: bar_position(x, pos_factor), + y: y, + width: width, + height: bottom - y, + color: color, + stroke: edge_color || color, + fill_opacity: alpha, + stroke_opacity: alpha, + stroke_width: line_width + } + ], + dataset, + bly + ) + end + def capture( [{x, y} | to_capture], captured, @@ -172,8 +208,6 @@ defmodule Matplotex.Figure.Areal.BarChart do x + pos_factor end - - defp list_of_ticks(data, step) do 1..length(data) |> Enum.into([], fn d -> diff --git a/lib/matplotex/figure/areal/plot_options.ex b/lib/matplotex/figure/areal/plot_options.ex index 1ff8001..e119752 100644 --- a/lib/matplotex/figure/areal/plot_options.ex +++ b/lib/matplotex/figure/areal/plot_options.ex @@ -21,6 +21,7 @@ defmodule Matplotex.Figure.Areal.PlotOptions do @spec set_options_in_figure(Figure.t(), keyword()) :: Figure.t() def set_options_in_figure(%Figure{} = figure, opts) do opts = sanitize(opts) + figure |> cast_figure(opts) |> cast_axes(opts) @@ -32,14 +33,19 @@ defmodule Matplotex.Figure.Areal.PlotOptions do end defp cast_axes(%Figure{axes: axes} = figure, opts) do - opts = Keyword.delete(opts, :label) cmap = Keyword.get(opts, :cmap) colors = Keyword.get(opts, :colors) + bottom = Keyword.get(opts, :bottom) %Figure{ figure - | axes: axes |> struct(opts) |> cast_two_d_structs(opts)|> update_cmap(cmap, colors) + | axes: + axes + |> struct(opts) + |> cast_two_d_structs(opts) + |> update_cmap(cmap, colors) + |> update_type(bottom) } end @@ -62,14 +68,19 @@ defmodule Matplotex.Figure.Areal.PlotOptions do defp sanitize(opts) do Keyword.drop(opts, @immutable_keys) end - defp update_cmap(axes, nil, colors) when is_list(colors) do + defp update_cmap(axes, nil, colors) when is_list(colors) do %{axes | cmap: Colormap.fetch_cmap(@default_cmap)} end defp update_cmap(axes, cmap, colors) when is_list(colors) do - %{axes | cmap: Colormap.fetch_cmap(cmap)} + %{axes | cmap: Colormap.fetch_cmap(cmap)} end - defp update_cmap(figure, _, _), do: figure + defp update_cmap(axes, _, _), do: axes + defp update_type(axes, nil), do: axes + + defp update_type(axes, bottom) when is_list(bottom) do + %{axes | type: "stacked"} + end end diff --git a/lib/matplotex/figure/areal/step.ex b/lib/matplotex/figure/areal/step.ex index 11889db..11568fb 100644 --- a/lib/matplotex/figure/areal/step.ex +++ b/lib/matplotex/figure/areal/step.ex @@ -1,5 +1,5 @@ defmodule Matplotex.Figure.Areal.Step do -@moduledoc false + @moduledoc false alias Matplotex.Figure.Areal.LinePlot alias Matplotex.Figure alias Matplotex.InputError diff --git a/lib/matplotex/figure/cast.ex b/lib/matplotex/figure/cast.ex index a9c94c2..801eadd 100644 --- a/lib/matplotex/figure/cast.ex +++ b/lib/matplotex/figure/cast.ex @@ -550,8 +550,11 @@ defmodule Matplotex.Figure.Cast do |> Algebra.flip_y_coordinate() |> Algebra.transform_given_point({0, height_region_content}) - {x2_cmap_tick, _} = tick_x2 = Algebra.transform_given_point(tick_coords, {tick_line_length, 0}) + {x2_cmap_tick, _} = + tick_x2 = Algebra.transform_given_point(tick_coords, {tick_line_length, 0}) + {tick_label_x, _} = Algebra.transform_given_point(tick_x2, {tick_line_length, 0}) + tick_label = Label.cast_label( %Label{type: "tick.cmap", x: tick_label_x, y: y_cord_tick, text: tick}, @@ -583,7 +586,8 @@ defmodule Matplotex.Figure.Cast do width: cmap_width, height: height_region_content } - }|>Cmap.color_gradient() + } + |> Cmap.color_gradient() ] ++ ticks end) |> List.flatten() @@ -593,7 +597,6 @@ defmodule Matplotex.Figure.Cast do def cast_legends(figure), do: figure - defp calculate_center(%Region{x: x, y: y, width: width}, :x) do {calculate_distance({x, y}, {x + width, y}) / 2 + x, y} end diff --git a/lib/matplotex/figure/dataset.ex b/lib/matplotex/figure/dataset.ex index ca84be5..ae23b1e 100644 --- a/lib/matplotex/figure/dataset.ex +++ b/lib/matplotex/figure/dataset.ex @@ -24,7 +24,8 @@ defmodule Matplotex.Figure.Dataset do marker: @default_marker, linestyle: @default_linestyle, marker_size: @default_marker_size, - line_width: @line_width + line_width: @line_width, + bottom: nil ] def cast(dataset, values) do diff --git a/lib/matplotex/figure/sketch.ex b/lib/matplotex/figure/sketch.ex index 5b4a1e0..dcd7338 100644 --- a/lib/matplotex/figure/sketch.ex +++ b/lib/matplotex/figure/sketch.ex @@ -19,8 +19,6 @@ defmodule Matplotex.Figure.Sketch do end}" end - - defp wrap_with_tag(svg, width, height) do ~s( <<_r :: binary-size(2), _g :: binary-size(2), _b :: binary-size(2)>>), do: true + defp is_color("#" <> <<_r::binary-size(2), _g::binary-size(2), _b::binary-size(2)>>), do: true defp is_color(_), do: false end diff --git a/test/matplotex/figure/areal/bar_chart_test.exs b/test/matplotex/figure/areal/bar_chart_test.exs index df39618..8afd591 100644 --- a/test/matplotex/figure/areal/bar_chart_test.exs +++ b/test/matplotex/figure/areal/bar_chart_test.exs @@ -15,8 +15,30 @@ defmodule Matplotex.Figure.Areal.BarChartTest do assert %Figure{axes: %{element: elements}} = BarChart.materialized_by_region(figure) |> BarChart.materialize() - assert assert Enum.filter(elements, fn x -> x.type == "figure.bar" end) |> length() == - length(y) + assert Enum.filter(elements, fn x -> x.type == "figure.bar" end) |> length() == + length(y) end + + test "bottom variable in opts makes stacked bar chart and it will stack the bars" do + values1 = [2, 4, 3, 2] + values2 = [1, 2, 1, 4] + values3 = [3, 1, 2, 1] + width = 0.3 + + bar = + Matplotex.bar(values1, width) + |> Matplotex.bar(values2, width, bottom: [values1]) + |> Matplotex.bar(values3, width, bottom: [values2, values1]) + + expected_elements_count = (values1 ++ values2 ++ values3) |> length() + assert %Figure{axes: %{element: elements}} = Figure.materialize(bar) + bar_elements = Enum.filter(elements, fn x -> x.type == "figure.bar" end) + assert bar_elements |> length() == expected_elements_count + assert Enum.all?(bar_elements, fn rect -> assert_valid_coords(rect) end) + end + end + + defp assert_valid_coords(rect) do + rect.x > 0 and rect.y > 0 and rect.width > 0 and rect.height > 0 end end diff --git a/test/matplotex/figure/areal/plot_options_test.exs b/test/matplotex/figure/areal/plot_options_test.exs index 807dc15..4595875 100644 --- a/test/matplotex/figure/areal/plot_options_test.exs +++ b/test/matplotex/figure/areal/plot_options_test.exs @@ -28,12 +28,15 @@ defmodule Matplotex.Figure.Areal.PlotOptionsTest do refute figure.axes.element == nil refute figure.axes == nil end + test "updates cmap as a list by its name" do - assert %Figure{axes: %{cmap: cmap}} = [1, 2, 3, 4, 5, 6] - |> Matplotex.plot([1, 2, 3, 4, 5], - cmap: "viridis", - colors: [3, 4, 5, 6, 7, 3, 2, 4] - ) + assert %Figure{axes: %{cmap: cmap}} = + [1, 2, 3, 4, 5, 6] + |> Matplotex.plot([1, 2, 3, 4, 5], + cmap: "viridis", + colors: [3, 4, 5, 6, 7, 3, 2, 4] + ) + assert is_list(cmap) end end