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