diff --git a/.gitignore b/.gitignore index 3b25aa0..1f592ae 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ plox-*.tar # Temporary files, for example, from tests. /tmp/ + +# Ignore "Desktop Services Store" hidden file +.DS_Store diff --git a/README.md b/README.md index 451d1aa..4bb6c48 100644 --- a/README.md +++ b/README.md @@ -58,16 +58,16 @@ Once you have those, you can render a `graph` component within your HEEx templat ```html <.graph id="example_graph" dimensions={@dimensions}> - <.x_axis_labels :let={date} axis={@x_axis}> + {Calendar.strftime(date, "%-m/%-d")} - + - <.y_axis_labels :let={value} axis={@y_axis} ticks={5}> + {value} - + - <.x_axis_grid_lines axis={@x_axis} stroke="#D3D3D3" /> - <.y_axis_grid_lines axis={@y_axis} ticks={5} stroke="#D3D3D3" /> + + <.polyline points={points(@dataset[:x], @dataset[:y])} stroke="#EC7E16" stroke-width={2} /> diff --git a/docs/migration_guide.md b/docs/migration_guide.md index 92debdb..62ba8ed 100644 --- a/docs/migration_guide.md +++ b/docs/migration_guide.md @@ -92,16 +92,26 @@ dataset = ```html <.graph id="example_graph" dimensions={@dimensions}> - <.x_axis_labels :let={date} axis={@x_axis}> + {Calendar.strftime(date, "%-m/%-d")} - + - <.y_axis_labels :let={value} axis={@y_axis} ticks={5}> + {value} - - - <.x_axis_grid_lines axis={@x_axis} stroke="#D3D3D3" /> - <.y_axis_grid_lines axis={@y_axis} ticks={5} stroke="#D3D3D3" /> + + + + + <.polyline points={points(@dataset[:x], @dataset[:y])} stroke="#EC7E16" stroke-width={2} /> diff --git a/examples/animated_demo_live.exs b/examples/animated_demo_live.exs index d073039..aa51d5c 100644 --- a/examples/animated_demo_live.exs +++ b/examples/animated_demo_live.exs @@ -13,6 +13,9 @@ defmodule AnimatedDemoLive do import Plox + alias Plox.Helpers.Axis + alias Plox.Helpers.Grid + @interval 1000 @impl Phoenix.LiveView @@ -83,26 +86,54 @@ defmodule AnimatedDemoLive do def render(assigns) do ~H""" <.graph dimensions={@dimensions}> - <.y_axis_labels :let={value} axis={@y_axis} ticks={5}> + {value} - + - <.y_axis_grid_lines axis={@y_axis} ticks={5} stroke="#D3D3D3" /> + - <.x_axis_labels :let={datetime} axis={@x_axis} step={5} start={@nearest_5_second}> + {Calendar.strftime(datetime, "%-I:%M:%S")} - + - <.x_axis_grid_lines axis={@x_axis} step={5} start={@nearest_5_second} stroke="#D3D3D3" /> - <.x_axis_grid_line axis={@x_axis} value={@x_axis.scale.first} stroke="#D3D3D3" /> - <.x_axis_grid_line axis={@x_axis} value={@x_axis.scale.last} stroke="#D3D3D3" /> + - <%!-- vertical marker for "now" with a label --%> - <.x_axis_label axis={@x_axis} value={@now} position={:top} stroke="red"> - Now ({Calendar.strftime(@now, "%-I:%M:%S")}) - + <%!-- draw left boundary of the graph --%> + - <.x_axis_grid_line axis={@x_axis} value={@now} stroke="red" /> + <%!-- draw right boundary of the graph --%> + + + <%!-- vertical marker for "now" with a label above the graph --%> + + ({Calendar.strftime(@now, "%-I:%M:%S")}) + + + <.polyline points={points(@dataset1[:x], @dataset1[:y])} stroke="orange" stroke-width="2" /> <.polyline points={points(@dataset2[:x], @dataset2[:y])} stroke="blue" stroke-width="2" /> diff --git a/examples/demo_live.exs b/examples/demo_live.exs index b33448c..627a1e4 100644 --- a/examples/demo_live.exs +++ b/examples/demo_live.exs @@ -13,6 +13,9 @@ defmodule DemoLive do import Plox + alias Plox.Helpers.Axis + alias Plox.Helpers.Grid + @impl Phoenix.LiveView def mount(_params, _session, socket) do {:ok, mount_simple_line_graph(socket)} @@ -69,22 +72,31 @@ defmodule DemoLive do

Example graph

<.graph dimensions={@dimensions}> - <.x_axis_labels :let={date} axis={@x_axis}> + <%!-- X-axis labels --%> + {Calendar.strftime(date, "%-m/%-d")} - - - <%!-- this wraps text... why does it take in `axis`?? if we want to follow the SVG, we need to pass in `x` --%> - <.x_axis_label axis={@x_axis} value={~D[2023-08-02]} position={:top} color="red"> - {"Important Day"} - - - <.x_axis_grid_lines axis={@x_axis} stroke="#D3D3D3" /> - - <.y_axis_labels :let={value} axis={@y_axis} ticks={5}> + + + <%!-- Add label for a specific date above the graph --%> + + Important Day + + + <%!-- X-axis grid lines --%> + + + <%!-- Y-axis labels --%> + {value} - + - <.y_axis_grid_lines axis={@y_axis} ticks={5} stroke="#D3D3D3" /> + <%!-- Y-axis grid lines --%> + <.polyline points={points(@dataset[:x], @dataset[:y])} stroke="orange" stroke-width={2} /> diff --git a/lib/plox.ex b/lib/plox.ex index a2b6f8d..8cac437 100644 --- a/lib/plox.ex +++ b/lib/plox.ex @@ -5,13 +5,9 @@ defmodule Plox do use Phoenix.Component + alias Plox.Constants alias Plox.Dimensions alias Plox.Scale - alias Plox.XAxis - alias Plox.YAxis - - # copied from SVG spec: https://svgwg.org/svg2-draft/styling.html#TermPresentationAttribute - @svg_presentation_globals ~w(alignment-baseline baseline-shift clip-path clip-rule color color-interpolation color-interpolation-filters cursor direction display dominant-baseline fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical image-rendering letter-spacing lighting-color marker-end marker-mid marker-start mask mask-type opacity overflow paint-order pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-overflow text-rendering transform-origin unicode-bidi vector-effect visibility white-space word-spacing writing-mode) @doc """ Entrypoint component for rendering graphs and plots. @@ -38,256 +34,6 @@ defmodule Plox do """ end - @doc """ - X-axis labels along the bottom or top of the graph. - - See `x_axis_label/1` for more details on the accepted attributes. - """ - @doc type: :component - - attr :axis, XAxis, required: true - attr :ticks, :any - attr :step, :any - attr :start, :any - attr :rest, :global, include: ~w(gap rotation position) ++ @svg_presentation_globals - - slot :inner_block, required: true - - def x_axis_labels(assigns) do - ~H""" - <.x_axis_label - :for={value <- Scale.values(@axis.scale, Map.take(assigns, [:ticks, :step, :start]))} - axis={@axis} - value={value} - {@rest} - > - {render_slot(@inner_block, value)} - - """ - end - - @doc """ - An X-axis label at the bottom or top of the graph. - """ - @doc type: :component - - attr :axis, XAxis, required: true - attr :value, :any, required: true - attr :position, :atom, values: [:top, :bottom], default: :bottom - attr :gap, :integer, default: 16 - attr :rotation, :integer, default: nil - attr :"dominant-baseline", :any, default: nil - attr :"text-anchor", :any, default: nil - attr :rest, :global, include: @svg_presentation_globals - - slot :inner_block, required: true - - def x_axis_label(%{position: :bottom} = assigns) do - ~H""" - - {render_slot(@inner_block)} - - """ - end - - def x_axis_label(%{position: :top} = assigns) do - ~H""" - - {render_slot(@inner_block)} - - """ - end - - @doc """ - Y-axis labels along the left or right side of the graph. - - See `y_axis_label/1` for more details on the accepeted attributes. - """ - @doc type: :component - - attr :axis, YAxis, required: true - attr :ticks, :any - attr :step, :any - attr :start, :any - attr :rest, :global, include: ~w(gap rotation position) ++ @svg_presentation_globals - - slot :inner_block, required: true - - def y_axis_labels(assigns) do - ~H""" - <.y_axis_label - :for={value <- Scale.values(@axis.scale, Map.take(assigns, [:ticks, :step, :start]))} - axis={@axis} - value={value} - {@rest} - > - {render_slot(@inner_block, value)} - - """ - end - - @doc """ - A Y-axis label at the left or right side of the graph. - """ - @doc type: :component - - attr :axis, YAxis, required: true - attr :value, :any, required: true - attr :position, :atom, values: [:left, :right], default: :left - attr :gap, :integer, default: 16 - attr :rotation, :integer, default: nil - attr :"dominant-baseline", :any, default: "middle" - attr :"text-anchor", :any, default: nil - attr :rest, :global, include: @svg_presentation_globals - - slot :inner_block, required: true - - def y_axis_label(%{position: :left} = assigns) do - ~H""" - - {render_slot(@inner_block)} - - """ - end - - def y_axis_label(%{position: :right} = assigns) do - ~H""" - - {render_slot(@inner_block)} - - """ - end - - @doc """ - X-axis grid lines. - """ - @doc type: :component - - attr :axis, XAxis, required: true - attr :ticks, :any - attr :step, :any - attr :start, :any - attr :rest, :global, include: @svg_presentation_globals - - def x_axis_grid_lines(assigns) do - ~H""" - <.x_axis_grid_line - :for={value <- Scale.values(@axis.scale, Map.take(assigns, [:ticks, :step, :start]))} - axis={@axis} - value={value} - {@rest} - /> - """ - end - - @doc """ - A single X-axis grid line. - """ - @doc type: :component - - attr :axis, XAxis, required: true - attr :value, :any, required: true - attr :top_overdraw, :integer, default: 0 - attr :bottom_overdraw, :integer, default: 0 - attr :rest, :global, include: @svg_presentation_globals - - def x_axis_grid_line(assigns) do - ~H""" - - """ - end - - @doc """ - Y-axis grid lines. - """ - @doc type: :component - - attr :axis, YAxis, required: true - attr :ticks, :any - attr :step, :any - attr :start, :any - attr :rest, :global, include: @svg_presentation_globals - - def y_axis_grid_lines(assigns) do - ~H""" - <.y_axis_grid_line - :for={value <- Scale.values(@axis.scale, Map.take(assigns, [:ticks, :step, :start]))} - axis={@axis} - value={value} - {@rest} - /> - """ - end - - @doc """ - A single Y-axis grid line. - """ - @doc type: :component - - attr :axis, YAxis, required: true - attr :value, :any, required: true - attr :rest, :global, include: @svg_presentation_globals - - def y_axis_grid_line(assigns) do - ~H""" - - """ - end - @doc """ Draws a SVG `` element connecting a series of points. """ @@ -295,7 +41,7 @@ defmodule Plox do attr :points, :any, required: true, doc: "String of coordinates (x1,y1 x2,y2...) or list of {x, y} tuples" attr :fill, :any, default: "none" - attr :rest, :global, include: @svg_presentation_globals + attr :rest, :global, include: Constants.svg_presentation_attrs() def polyline(%{points: points} = assigns) when is_binary(points), do: do_polyline(assigns) @@ -323,7 +69,7 @@ defmodule Plox do attr :points, :any, required: true, doc: "String of coordinates (x1,y1 x2,y2...) or list of {x, y} tuples" attr :fill, :any, default: "none" - attr :rest, :global, include: @svg_presentation_globals + attr :rest, :global, include: Constants.svg_presentation_attrs() def step_polyline(%{points: points} = assigns) when is_binary(points) do points = @@ -368,14 +114,13 @@ defmodule Plox do """ @doc type: :component - # TODO: I wonder if we can more dynamically determine all "dynamic"-possible attributes attr :cx, :any, required: true attr :cy, :any, required: true attr :r, :any, required: true attr :fill, :any, default: nil attr :stroke, :any, default: nil attr :"stroke-width", :any, default: nil - attr :rest, :global, include: @svg_presentation_globals + attr :rest, :global, include: Constants.svg_presentation_attrs() def circle(assigns) do ~H""" @@ -465,6 +210,71 @@ defmodule Plox do end end + @doc """ + Returns scale values for rendering labels and grid lines. + + ## Example + + iex> scale_values(x_axis, ticks: 5) + [~D[2023-08-01], ~D[2023-08-02], ...] + """ + def scale_values(%{scale: scale}, opts \\ []) do + opts = Map.new(opts) + Scale.values(scale, opts) + end + + @doc """ + Returns the y-coordinate for positioning elements above the graph (e.g. x-axis labels at top). + See `Plox.Constants.default_label_gap/0` for default gap value. + """ + def above_graph(dimensions, gap \\ Constants.default_label_gap()) do + dimensions.margin.top + dimensions.padding.top - gap + end + + @doc """ + Returns the y-coordinate for positioning elements below the graph (e.g. x-axis labels at bottom). + See `Plox.Constants.default_label_gap/0` for default gap value. + """ + def below_graph(dimensions, gap \\ Constants.default_label_gap()) do + dimensions.height - dimensions.margin.bottom - dimensions.padding.bottom + gap + end + + @doc """ + Returns the x-coordinate for positioning elements to the left of the graph (e.g. y-axis labels). + See `Plox.Constants.default_label_gap/0` for default gap value. + """ + def left_of_graph(dimensions, gap \\ Constants.default_label_gap()) do + dimensions.margin.left + dimensions.padding.left - gap + end + + @doc """ + Returns the x-coordinate for positioning elements to the right of the graph (e.g. y-axis labels). + See `Plox.Constants.default_label_gap/0` for default gap value. + """ + def right_of_graph(dimensions, gap \\ Constants.default_label_gap()) do + dimensions.width - dimensions.margin.right - dimensions.padding.right + gap + end + + @doc """ + Returns the top boundary of the graph area (for grid lines and other elements). + """ + def graph_top(dimensions), do: dimensions.margin.top + dimensions.padding.top + + @doc """ + Returns the bottom boundary of the graph area (for grid lines and other elements). + """ + def graph_bottom(dimensions), do: dimensions.height - dimensions.margin.bottom - dimensions.padding.bottom + + @doc """ + Returns the left boundary of the graph area (for grid lines and other elements). + """ + def graph_left(dimensions), do: dimensions.margin.left + dimensions.padding.left + + @doc """ + Returns the right boundary of the graph area (for grid lines and other elements). + """ + def graph_right(dimensions), do: dimensions.width - dimensions.margin.right - dimensions.padding.right + # @doc """ # Bar plot. # """ diff --git a/lib/plox/constants.ex b/lib/plox/constants.ex new file mode 100644 index 0000000..3cc9327 --- /dev/null +++ b/lib/plox/constants.ex @@ -0,0 +1,81 @@ +defmodule Plox.Constants do + @moduledoc """ + Constants used throughout Plox components. + """ + + @doc """ + SVG presentation attributes that can be passed via @rest in components. + + See: https://svgwg.org/svg2-draft/styling.html#TermPresentationAttribute + """ + def svg_presentation_attrs do + ~w( + alignment-baseline + baseline-shift + clip-path + clip-rule + color + color-interpolation + color-interpolation-filters + cursor + direction + display + dominant-baseline + fill + fill-opacity + fill-rule + filter + flood-color + flood-opacity + font-family + font-size + font-size-adjust + font-stretch + font-style + font-variant + font-weight + glyph-orientation-horizontal + glyph-orientation-vertical + image-rendering + letter-spacing + lighting-color + marker-end + marker-mid + marker-start + mask + mask-type + opacity + overflow + paint-order + pointer-events + shape-rendering + stop-color + stop-opacity + stroke + stroke-dasharray + stroke-dashoffset + stroke-linecap + stroke-linejoin + stroke-miterlimit + stroke-opacity + stroke-width + text-anchor + text-decoration + text-overflow + text-rendering + transform + transform-origin + unicode-bidi + vector-effect + visibility + white-space + word-spacing + writing-mode + ) + end + + @doc """ + Default gap between graph boundary and labels (in pixels). + """ + def default_label_gap, do: 16 +end diff --git a/lib/plox/helpers/axis.ex b/lib/plox/helpers/axis.ex new file mode 100644 index 0000000..69ff5b8 --- /dev/null +++ b/lib/plox/helpers/axis.ex @@ -0,0 +1,125 @@ +defmodule Plox.Helpers.Axis do + @moduledoc """ + Helper components for rendering axis labels. + + These components wrap common patterns for axis labels using standard SVG elements + and Plox helper functions. They are convenience wrappers - you can always drop down + to raw SVG for more control. + """ + + use Phoenix.Component + + import Plox + + alias Plox.Constants + + @doc """ + Renders multiple labels below or above the graph, along the given + `Plox.XAxis`. Defaults to below. + + Not for use when rendering single labels. It is recommended to use + SVG `` elements directly for that purpose. + + ## Examples + + + {Calendar.strftime(date, "%-m/%-d")} + + """ + attr :axis, Plox.XAxis, required: true + attr :position, :atom, default: :below, values: [:below, :top] + attr :gap, :integer, default: Constants.default_label_gap() + attr :ticks, :integer, doc: "Optional number of labels to render (not to be used with `:step`)" + attr :step, :any, doc: "Optional size of step between label values (not to be used with `:ticks`)" + attr :start, :any, doc: "Optional starting value for labels" + attr :"dominant-baseline", :any, default: nil + attr :"text-anchor", :any, default: "middle" + attr :rest, :global, include: Constants.svg_presentation_attrs() + + slot :inner_block, required: true + + def x_labels(%{position: :below} = assigns) do + ~H""" + + {render_slot(@inner_block, value)} + + """ + end + + def x_labels(%{position: :top} = assigns) do + ~H""" + + {render_slot(@inner_block, value)} + + """ + end + + @doc """ + Renders multiple labels on the left or right of the graph, along + the given `Plox.YAxis`. Defaults to the left. + + Not for use when rendering single labels. It is recommended to use + SVG `` elements directly for that purpose. + + ## Examples + + + {value} + + """ + attr :axis, Plox.YAxis, required: true + attr :position, :atom, default: :left, values: [:left, :right] + attr :gap, :integer, default: Constants.default_label_gap() + attr :ticks, :integer, doc: "Optional number of labels to render (not to be used with `:step`)" + attr :step, :any, doc: "Optional size of step between label values (not to be used with `:ticks`)" + attr :start, :any, doc: "Optional starting value for labels" + attr :"dominant-baseline", :any, default: "middle" + attr :"text-anchor", :any, default: nil + attr :rest, :global, include: Constants.svg_presentation_attrs() + + slot :inner_block, required: true + + def y_labels(%{position: :left} = assigns) do + ~H""" + + {render_slot(@inner_block, value)} + + """ + end + + def y_labels(%{position: :right} = assigns) do + ~H""" + + {render_slot(@inner_block, value)} + + """ + end +end diff --git a/lib/plox/helpers/grid.ex b/lib/plox/helpers/grid.ex new file mode 100644 index 0000000..1d130cd --- /dev/null +++ b/lib/plox/helpers/grid.ex @@ -0,0 +1,73 @@ +defmodule Plox.Helpers.Grid do + @moduledoc """ + Helper components for rendering grid lines. + + These components wrap common patterns for grid lines using standard SVG elements + and Plox helper functions. They are convenience wrappers - you can always drop down + to raw SVG for more control. + """ + + use Phoenix.Component + + import Plox + + alias Plox.Constants + + @doc """ + Renders vertical lines at values along the given `axis`. + + ## Examples + + <.vertical_lines axis={@x_axis} dimensions={@dimensions} ticks={5} /> + """ + attr :axis, :any, required: true + attr :dimensions, :any, required: true + attr :ticks, :integer, doc: "Optional number of lines to render (not to be used with `:step`)" + attr :step, :any, doc: "Optional size of step between line values (not to be used with `:ticks`)" + attr :start, :any, doc: "Optional starting value for lines" + attr :stroke, :string, default: "#D3D3D3" + attr :rest, :global, include: Constants.svg_presentation_attrs() + + def vertical_lines(assigns) do + ~H""" + + """ + end + + @doc """ + Renders horizontal lines at values along the given `axis`. + + ## Examples + + <.horizontal_lines axis={@y_axis} dimensions={@dimensions} ticks={5} /> + """ + attr :axis, :any, required: true + attr :dimensions, :any, required: true + attr :ticks, :integer, doc: "Optional number of lines to render (not to be used with `:step`)" + attr :step, :any, doc: "Optional size of step between line values (not to be used with `:ticks`)" + attr :start, :any, doc: "Optional starting value for lines" + attr :stroke, :string, default: "#D3D3D3" + attr :rest, :global, include: Constants.svg_presentation_attrs() + + def horizontal_lines(assigns) do + ~H""" + + """ + end +end diff --git a/mix.exs b/mix.exs index 890786d..a2ed6b4 100644 --- a/mix.exs +++ b/mix.exs @@ -79,6 +79,10 @@ defmodule Plox.MixProject do ], "Color Scales": [ Plox.FixedColorsScale + ], + Helpers: [ + Plox.Helpers.Axis, + Plox.Helpers.Grid ] ] end diff --git a/test/plox_test.exs b/test/plox_test.exs index 94eebac..349040a 100644 --- a/test/plox_test.exs +++ b/test/plox_test.exs @@ -50,4 +50,149 @@ defmodule PloxTest do assert Plox.values([[1, 2], dataset[:y]]) == [{1.0, 80.0}, {2.0, 70.0}] end end + + describe "scale_values/2" do + test "delegates to Scale.values/2 with no options" do + scale = Plox.NumberScale.new(0, 10) + axis = Plox.XAxis.new(scale, Plox.Dimensions.new(100, 100)) + + assert Plox.scale_values(axis) == Plox.Scale.values(scale) + end + + test "delegates to Scale.values/2 with ticks" do + scale = Plox.NumberScale.new(0, 10) + axis = Plox.XAxis.new(scale, Plox.Dimensions.new(100, 100)) + + assert Plox.scale_values(axis, %{ticks: 5}) == Plox.Scale.values(scale, %{ticks: 5}) + end + + test "delegates to Scale.values/2 with step" do + scale = Plox.DateScale.new(Date.range(~D[2019-01-01], ~D[2019-01-10])) + axis = Plox.XAxis.new(scale, Plox.Dimensions.new(100, 100)) + + assert Plox.scale_values(axis, %{step: 2}) == Plox.Scale.values(scale, %{step: 2}) + end + + test "delegates to Scale.values/2 with step tuple" do + scale = Plox.DateTimeScale.new(~N[2019-01-01 00:00:00], ~N[2019-01-01 00:10:00]) + axis = Plox.XAxis.new(scale, Plox.Dimensions.new(100, 100)) + + assert Plox.scale_values(axis, %{step: {2, :minute}}) == + Plox.Scale.values(scale, %{step: {2, :minute}}) + end + + test "delegates to Scale.values/2 with start" do + scale = Plox.DateTimeScale.new(~N[2019-01-01 00:00:00], ~N[2019-01-01 00:02:00]) + axis = Plox.XAxis.new(scale, Plox.Dimensions.new(100, 100)) + + assert Plox.scale_values(axis, %{start: ~N[2019-01-01 00:01:00]}) == + Plox.Scale.values(scale, %{start: ~N[2019-01-01 00:01:00]}) + end + end + + describe "positional helper functions" do + setup do + margins = {50, 40, 60, 70} + paddings = {10, 20, 30, 40} + dimensions = Plox.Dimensions.new(800, 600, margin: margins, padding: paddings) + + %{dimensions: dimensions, margins: margins, paddings: paddings} + end + + test "above_graph/1 returns y-coordinate above graph with default gap", %{ + dimensions: dimensions, + margins: {mt, _mr, _mb, _ml}, + paddings: {pt, _pr, _pb, _pl} + } do + assert Plox.above_graph(dimensions) == mt + pt - Plox.Constants.default_label_gap() + end + + test "above_graph/2 returns y-coordinate above graph with custom gap", %{ + dimensions: dimensions, + margins: {mt, _mr, _mb, _ml}, + paddings: {pt, _pr, _pb, _pl} + } do + assert Plox.above_graph(dimensions, 20) == mt + pt - 20 + end + + test "below_graph/1 returns y-coordinate below graph with default gap", %{ + dimensions: dimensions, + margins: {_mt, _mr, mb, _ml}, + paddings: {_pt, _pr, pb, _pl} + } do + assert Plox.below_graph(dimensions) == 600 - mb - pb + Plox.Constants.default_label_gap() + end + + test "below_graph/2 returns y-coordinate below graph with custom gap", %{ + dimensions: dimensions, + margins: {_mt, _mr, mb, _ml}, + paddings: {_pt, _pr, pb, _pl} + } do + assert Plox.below_graph(dimensions, 15) == 600 - mb - pb + 15 + end + + test "left_of_graph/1 returns x-coordinate left of graph with default gap", %{ + dimensions: dimensions, + margins: {_mt, _mr, _mb, ml}, + paddings: {_pt, _pr, _pb, pl} + } do + assert Plox.left_of_graph(dimensions) == ml + pl - Plox.Constants.default_label_gap() + end + + test "left_of_graph/2 returns x-coordinate left of graph with custom gap", %{ + dimensions: dimensions, + margins: {_mt, _mr, _mb, ml}, + paddings: {_pt, _pr, _pb, pl} + } do + assert Plox.left_of_graph(dimensions, 10) == ml + pl - 10 + end + + test "right_of_graph/1 returns x-coordinate right of graph with default gap", %{ + dimensions: dimensions, + margins: {_mt, mr, _mb, _ml}, + paddings: {_pt, pr, _pb, _pl} + } do + assert Plox.right_of_graph(dimensions) == 800 - mr - pr + Plox.Constants.default_label_gap() + end + + test "right_of_graph/2 returns x-coordinate right of graph with custom gap", %{ + dimensions: dimensions, + margins: {_mt, mr, _mb, _ml}, + paddings: {_pt, pr, _pb, _pl} + } do + assert Plox.right_of_graph(dimensions, 20) == 800 - mr - pr + 20 + end + + test "graph_top/1 returns top boundary of graph area", %{ + dimensions: dimensions, + margins: {mt, _mr, _mb, _ml}, + paddings: {pt, _pr, _pb, _pl} + } do + assert Plox.graph_top(dimensions) == mt + pt + end + + test "graph_bottom/1 returns bottom boundary of graph area", %{ + dimensions: dimensions, + margins: {_mt, _mr, mb, _ml}, + paddings: {_pt, _pr, pb, _pl} + } do + assert Plox.graph_bottom(dimensions) == 600 - mb - pb + end + + test "graph_left/1 returns left boundary of graph area", %{ + dimensions: dimensions, + margins: {_mt, _mr, _mb, ml}, + paddings: {_pt, _pr, _pb, pl} + } do + assert Plox.graph_left(dimensions) == ml + pl + end + + test "graph_right/1 returns right boundary of graph area", %{ + dimensions: dimensions, + margins: {_mt, mr, _mb, _ml}, + paddings: {_pt, pr, _pb, _pl} + } do + assert Plox.graph_right(dimensions) == 800 - mr - pr + end + end end