From 2668c8ef2090f1fe71950978c3f1a9f9c31c9eb4 Mon Sep 17 00:00:00 2001 From: Nikki Kyllonen Date: Fri, 16 Jan 2026 14:33:43 -0800 Subject: [PATCH 01/16] Add text and line components + position helper fns --- .gitignore | 3 ++ lib/plox.ex | 148 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+) 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/lib/plox.ex b/lib/plox.ex index a2b6f8d..e4d0fdf 100644 --- a/lib/plox.ex +++ b/lib/plox.ex @@ -288,6 +288,89 @@ defmodule Plox do """ end + @doc """ + Draws a single or set of SVG `` elements. + """ + @doc type: :component + + attr :x, :any, required: true + attr :y, :any, required: true + attr :"text-anchor", :any, default: nil + attr :"dominant-baseline", :any, default: nil + attr :fill, :any, default: nil + attr :"font-size", :any, default: nil + attr :rest, :global, include: Constants.svg_presentation_attrs() + + slot :inner_block + + def text(assigns) do + ~H""" + + {render_slot(@inner_block)} + + """ + end + + @doc """ + Draws a single or set of SVG `` elements. + """ + @doc type: :component + + attr :x1, :any, required: true + attr :y1, :any, required: true + attr :x2, :any, required: true + attr :y2, :any, required: true + attr :stroke, :any, default: nil + attr :"stroke-width", :any, default: nil + attr :"stroke-dasharray", :any, default: nil + attr :rest, :global, include: Constants.svg_presentation_attrs() + + def line(assigns) do + ~H""" + + """ + end + @doc """ Draws a SVG `` element connecting a series of points. """ @@ -465,6 +548,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 \\ 16) do + dimensions.margin.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 + 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 \\ 16) do + dimensions.margin.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 \\ 16) do + dimensions.width - dimensions.margin.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 + + @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 + + @doc """ + Returns the left boundary of the graph area (for grid lines and other elements). + """ + def graph_left(dimensions), do: dimensions.margin.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 + # @doc """ # Bar plot. # """ From 9668d44c3cc21f5afd6997de35007c3fe800dbfa Mon Sep 17 00:00:00 2001 From: Nikki Kyllonen Date: Fri, 16 Jan 2026 14:34:21 -0800 Subject: [PATCH 02/16] Update demo_live to use new text and line components for labels and gridlines --- examples/demo_live.exs | 59 +++++++++++++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 12 deletions(-) diff --git a/examples/demo_live.exs b/examples/demo_live.exs index b33448c..82c6c71 100644 --- a/examples/demo_live.exs +++ b/examples/demo_live.exs @@ -69,22 +69,57 @@ defmodule DemoLive do

Example graph

<.graph dimensions={@dimensions}> - <.x_axis_labels :let={date} axis={@x_axis}> + <%!-- X-axis labels --%> + <.text + :for={date <- scale_values(@x_axis, ticks: 5)} + x={@x_axis[date]} + y={below_graph(@dimensions)} + dominant-baseline="hanging" + text-anchor="middle" + > {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"> + + + <%!-- Add label for a specific date above the graph --%> + <.text + x={@x_axis[~D[2023-08-02]]} + y={above_graph(@dimensions)} + dominant-baseline="text-bottom" + text-anchor="middle" + > {"Important Day"} - - - <.x_axis_grid_lines axis={@x_axis} stroke="#D3D3D3" /> + + + <%!-- X-axis grid lines --%> + <.line + :for={date <- scale_values(@x_axis, ticks: 5)} + x1={@x_axis[date]} + y1={graph_top(@dimensions)} + x2={@x_axis[date]} + y2={graph_bottom(@dimensions)} + stroke="#D3D3D3" + /> - <.y_axis_labels :let={value} axis={@y_axis} ticks={5}> + <%!-- Y-axis labels --%> + <.text + :for={value <- scale_values(@y_axis, ticks: 5)} + x={left_of_graph(@dimensions)} + y={@y_axis[value]} + dominant-baseline="middle" + text-anchor="end" + > {value} - - - <.y_axis_grid_lines axis={@y_axis} ticks={5} stroke="#D3D3D3" /> + + + <%!-- Y-axis grid lines --%> + <.line + :for={value <- scale_values(@y_axis, ticks: 5)} + x1={graph_left(@dimensions)} + y1={@y_axis[value]} + x2={graph_right(@dimensions)} + y2={@y_axis[value]} + stroke="#D3D3D3" + /> <.polyline points={points(@dataset[:x], @dataset[:y])} stroke="orange" stroke-width={2} /> From dbd623ddc50cd3645f7eb1f9299a3deaf738ef28 Mon Sep 17 00:00:00 2001 From: Nikki Kyllonen Date: Fri, 16 Jan 2026 14:35:11 -0800 Subject: [PATCH 03/16] Add Constants module for organization --- lib/plox.ex | 39 ++++++++++----------- lib/plox/constants.ex | 81 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 21 deletions(-) create mode 100644 lib/plox/constants.ex diff --git a/lib/plox.ex b/lib/plox.ex index e4d0fdf..9cfa61b 100644 --- a/lib/plox.ex +++ b/lib/plox.ex @@ -5,14 +5,12 @@ 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. """ @@ -49,7 +47,7 @@ defmodule Plox do attr :ticks, :any attr :step, :any attr :start, :any - attr :rest, :global, include: ~w(gap rotation position) ++ @svg_presentation_globals + attr :rest, :global, include: ~w(gap rotation position) ++ Constants.svg_presentation_attrs() slot :inner_block, required: true @@ -77,8 +75,8 @@ defmodule Plox do 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 + attr :"text-anchor", :any, default: "middle" + attr :rest, :global, include: Constants.svg_presentation_attrs() slot :inner_block, required: true @@ -88,7 +86,7 @@ defmodule Plox do x={x = @axis[@value]} y={@axis.dimensions.height - @axis.dimensions.margin.bottom + @gap} dominant-baseline={assigns[:"dominant-baseline"] || "hanging"} - text-anchor={assigns[:"text-anchor"] || "middle"} + text-anchor={assigns[:"text-anchor"]} transform={ if @rotation, do: @@ -107,7 +105,7 @@ defmodule Plox do x={x = @axis[@value]} y={@axis.dimensions.margin.bottom - @gap} dominant-baseline={assigns[:"dominant-baseline"] || "text-bottom"} - text-anchor={assigns[:"text-anchor"] || "middle"} + text-anchor={assigns[:"text-anchor"]} transform={ if @rotation, do: "rotate(#{@rotation}, #{x}, #{@axis.dimensions.margin.bottom - @gap})" @@ -130,7 +128,7 @@ defmodule Plox do attr :ticks, :any attr :step, :any attr :start, :any - attr :rest, :global, include: ~w(gap rotation position) ++ @svg_presentation_globals + attr :rest, :global, include: ~w(gap rotation position) ++ Constants.svg_presentation_attrs() slot :inner_block, required: true @@ -159,7 +157,7 @@ defmodule Plox do attr :rotation, :integer, default: nil attr :"dominant-baseline", :any, default: "middle" attr :"text-anchor", :any, default: nil - attr :rest, :global, include: @svg_presentation_globals + attr :rest, :global, include: Constants.svg_presentation_attrs() slot :inner_block, required: true @@ -209,7 +207,7 @@ defmodule Plox do attr :ticks, :any attr :step, :any attr :start, :any - attr :rest, :global, include: @svg_presentation_globals + attr :rest, :global, include: Constants.svg_presentation_attrs() def x_axis_grid_lines(assigns) do ~H""" @@ -231,7 +229,7 @@ defmodule Plox do attr :value, :any, required: true attr :top_overdraw, :integer, default: 0 attr :bottom_overdraw, :integer, default: 0 - attr :rest, :global, include: @svg_presentation_globals + attr :rest, :global, include: Constants.svg_presentation_attrs() def x_axis_grid_line(assigns) do ~H""" @@ -254,7 +252,7 @@ defmodule Plox do attr :ticks, :any attr :step, :any attr :start, :any - attr :rest, :global, include: @svg_presentation_globals + attr :rest, :global, include: Constants.svg_presentation_attrs() def y_axis_grid_lines(assigns) do ~H""" @@ -274,7 +272,7 @@ defmodule Plox do attr :axis, YAxis, required: true attr :value, :any, required: true - attr :rest, :global, include: @svg_presentation_globals + attr :rest, :global, include: Constants.svg_presentation_attrs() def y_axis_grid_line(assigns) do ~H""" @@ -378,7 +376,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) @@ -406,7 +404,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 = @@ -451,14 +449,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""" @@ -565,7 +562,7 @@ defmodule Plox do 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 \\ 16) do + def above_graph(dimensions, gap \\ Constants.default_label_gap()) do dimensions.margin.top - gap end @@ -581,7 +578,7 @@ defmodule Plox do 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 \\ 16) do + def left_of_graph(dimensions, gap \\ Constants.default_label_gap()) do dimensions.margin.left - gap end @@ -589,7 +586,7 @@ defmodule Plox do 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 \\ 16) do + def right_of_graph(dimensions, gap \\ Constants.default_label_gap()) do dimensions.width - dimensions.margin.right + gap end 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 From f0dc91a85b66422b2b689fa7be79b6a286c963aa Mon Sep 17 00:00:00 2001 From: Nikki Kyllonen Date: Fri, 16 Jan 2026 15:05:45 -0800 Subject: [PATCH 04/16] Replace new text/line components with text/line SVG elements --- examples/demo_live.exs | 16 ++++---- lib/plox.ex | 83 ------------------------------------------ 2 files changed, 8 insertions(+), 91 deletions(-) diff --git a/examples/demo_live.exs b/examples/demo_live.exs index 82c6c71..26c4b22 100644 --- a/examples/demo_live.exs +++ b/examples/demo_live.exs @@ -70,7 +70,7 @@ defmodule DemoLive do <.graph dimensions={@dimensions}> <%!-- X-axis labels --%> - <.text + {Calendar.strftime(date, "%-m/%-d")} - + <%!-- Add label for a specific date above the graph --%> - <.text + {"Important Day"} - + <%!-- X-axis grid lines --%> - <.line + <%!-- Y-axis labels --%> - <.text + {value} - + <%!-- Y-axis grid lines --%> - <.line + ` elements. - """ - @doc type: :component - - attr :x, :any, required: true - attr :y, :any, required: true - attr :"text-anchor", :any, default: nil - attr :"dominant-baseline", :any, default: nil - attr :fill, :any, default: nil - attr :"font-size", :any, default: nil - attr :rest, :global, include: Constants.svg_presentation_attrs() - - slot :inner_block - - def text(assigns) do - ~H""" - - {render_slot(@inner_block)} - - """ - end - - @doc """ - Draws a single or set of SVG `` elements. - """ - @doc type: :component - - attr :x1, :any, required: true - attr :y1, :any, required: true - attr :x2, :any, required: true - attr :y2, :any, required: true - attr :stroke, :any, default: nil - attr :"stroke-width", :any, default: nil - attr :"stroke-dasharray", :any, default: nil - attr :rest, :global, include: Constants.svg_presentation_attrs() - - def line(assigns) do - ~H""" - - """ - end - @doc """ Draws a SVG `` element connecting a series of points. """ From 700b160c3c543124a784f0c4f9ea77d453b6b81f Mon Sep 17 00:00:00 2001 From: Nikki Kyllonen Date: Tue, 20 Jan 2026 12:11:05 -0800 Subject: [PATCH 05/16] Add new Axis and Grid helper modules --- lib/plox/helpers/axis.ex | 121 +++++++++++++++++++++++++++++++++++++++ lib/plox/helpers/grid.ex | 63 ++++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 lib/plox/helpers/axis.ex create mode 100644 lib/plox/helpers/grid.ex diff --git a/lib/plox/helpers/axis.ex b/lib/plox/helpers/axis.ex new file mode 100644 index 0000000..85f7845 --- /dev/null +++ b/lib/plox/helpers/axis.ex @@ -0,0 +1,121 @@ +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 X-axis labels below or above the graph. + 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, :any, required: true + attr :dimensions, :any, required: true + attr :position, :atom, default: :below, values: [:below, :top] + attr :gap, :integer, default: Constants.default_label_gap() + attr :ticks, :integer, required: true + attr :step, :any + 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 Y-axis labels on the left or right of the graph. + 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, :any, required: true + attr :dimensions, :any, required: true + attr :position, :atom, default: :left, values: [:left, :right] + attr :gap, :integer, default: Constants.default_label_gap() + attr :ticks, :integer, required: true + attr :step, :any + 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..da2c1e4 --- /dev/null +++ b/lib/plox/helpers/grid.ex @@ -0,0 +1,63 @@ +defmodule Plox.Helpers.Grid do + @moduledoc """ + Helper components for rendering grid lines. + + 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 vertical grid lines at X-axis tick positions. + """ + attr :axis, :any, required: true + attr :dimensions, :any, required: true + attr :ticks, :integer, required: true + attr :step, :any + attr :stroke, :string, default: "#D3D3D3" + attr :rest, :global, include: Constants.svg_presentation_attrs() + + def x_lines(assigns) do + ~H""" + + """ + end + + @doc """ + Renders horizontal grid lines at Y-axis tick positions. + """ + attr :axis, :any, required: true + attr :dimensions, :any, required: true + attr :ticks, :integer, required: true + attr :step, :any + attr :stroke, :string, default: "#D3D3D3" + attr :rest, :global, include: Constants.svg_presentation_attrs() + + def y_lines(assigns) do + ~H""" + + """ + end +end From 972454af19483670687f87a058eb79ab19df2e98 Mon Sep 17 00:00:00 2001 From: Nikki Kyllonen Date: Tue, 20 Jan 2026 12:11:25 -0800 Subject: [PATCH 06/16] Update DemoLive to use new Axis and Grid helpers --- examples/demo_live.exs | 44 ++++++++++-------------------------------- 1 file changed, 10 insertions(+), 34 deletions(-) diff --git a/examples/demo_live.exs b/examples/demo_live.exs index 26c4b22..bee49a4 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,16 +72,9 @@ defmodule DemoLive do

Example graph

<.graph dimensions={@dimensions}> - <%!-- X-axis labels --%> - + {Calendar.strftime(date, "%-m/%-d")} - + <%!-- Add label for a specific date above the graph --%> - {"Important Day"} + Important Day <%!-- X-axis grid lines --%> - + <%!-- Y-axis labels --%> - + {value} - + <%!-- Y-axis grid lines --%> - + <.polyline points={points(@dataset[:x], @dataset[:y])} stroke="orange" stroke-width={2} /> From 3445a09d2dee959b5133c11da1f4f3a2c81ddf5c Mon Sep 17 00:00:00 2001 From: Nikki Kyllonen Date: Tue, 20 Jan 2026 16:41:48 -0800 Subject: [PATCH 07/16] Update new Axis and Grid helpers --- examples/demo_live.exs | 5 +++-- lib/plox/helpers/axis.ex | 28 +++++++++++++++------------- lib/plox/helpers/grid.ex | 30 ++++++++++++++++++++---------- mix.exs | 4 ++++ 4 files changed, 42 insertions(+), 25 deletions(-) diff --git a/examples/demo_live.exs b/examples/demo_live.exs index bee49a4..c39a754 100644 --- a/examples/demo_live.exs +++ b/examples/demo_live.exs @@ -72,6 +72,7 @@ defmodule DemoLive do

Example graph

<.graph dimensions={@dimensions}> + <%!-- X-axis labels --%> {Calendar.strftime(date, "%-m/%-d")} @@ -87,7 +88,7 @@ defmodule DemoLive do
<%!-- X-axis grid lines --%> - + <%!-- Y-axis labels --%> @@ -95,7 +96,7 @@ defmodule DemoLive do <%!-- Y-axis grid lines --%> - + <.polyline points={points(@dataset[:x], @dataset[:y])} stroke="orange" stroke-width={2} /> diff --git a/lib/plox/helpers/axis.ex b/lib/plox/helpers/axis.ex index 85f7845..dbc2a5f 100644 --- a/lib/plox/helpers/axis.ex +++ b/lib/plox/helpers/axis.ex @@ -14,8 +14,8 @@ defmodule Plox.Helpers.Axis do alias Plox.Constants @doc """ - Renders multiple X-axis labels below or above the graph. - Defaults to below. + Renders multiple labels below or above the graph, along the given + `axis`. Defaults to below. Not for use when rendering single labels. It is recommended to use SVG `` elements directly for that purpose. @@ -23,15 +23,16 @@ defmodule Plox.Helpers.Axis do ## Examples - <%= Calendar.strftime(date, "%-m/%-d") %> + {Calendar.strftime(date, "%-m/%-d")} """ attr :axis, :any, required: true attr :dimensions, :any, required: true attr :position, :atom, default: :below, values: [:below, :top] attr :gap, :integer, default: Constants.default_label_gap() - attr :ticks, :integer, required: true - attr :step, :any + 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 :rest, :global, include: Constants.svg_presentation_attrs() slot :inner_block, required: true @@ -39,7 +40,7 @@ defmodule Plox.Helpers.Axis do def x_labels(%{position: :below} = assigns) do ~H""" ` elements directly for that purpose. @@ -83,8 +84,9 @@ defmodule Plox.Helpers.Axis do attr :dimensions, :any, required: true attr :position, :atom, default: :left, values: [:left, :right] attr :gap, :integer, default: Constants.default_label_gap() - attr :ticks, :integer, required: true - attr :step, :any + 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 :rest, :global, include: Constants.svg_presentation_attrs() slot :inner_block, required: true @@ -92,7 +94,7 @@ defmodule Plox.Helpers.Axis do def y_labels(%{position: :left} = assigns) do ~H""" """ attr :axis, :any, required: true attr :dimensions, :any, required: true - attr :ticks, :integer, required: true - attr :step, :any + 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 x_lines(assigns) do + def vertical_lines(assigns) do ~H""" """ attr :axis, :any, required: true attr :dimensions, :any, required: true - attr :ticks, :integer, required: true - attr :step, :any + 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 y_lines(assigns) do + def horizontal_lines(assigns) do ~H""" Date: Tue, 20 Jan 2026 16:42:24 -0800 Subject: [PATCH 08/16] Update AnimatedDemoLive to use new Axis and Grid helpers --- examples/animated_demo_live.exs | 63 ++++++++++++++++++++++++++------- 1 file changed, 50 insertions(+), 13 deletions(-) diff --git a/examples/animated_demo_live.exs b/examples/animated_demo_live.exs index d073039..28c72f2 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,60 @@ 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" /> From c14d849ccc7e5064d738c295d2293f9a5529dac9 Mon Sep 17 00:00:00 2001 From: Nikki Kyllonen Date: Wed, 21 Jan 2026 09:27:51 -0800 Subject: [PATCH 09/16] Remove dimensions from Axis label helpers and require XAxis or YAxis specifically --- examples/animated_demo_live.exs | 10 ++-------- examples/demo_live.exs | 4 ++-- lib/plox/helpers/axis.ex | 22 ++++++++++------------ 3 files changed, 14 insertions(+), 22 deletions(-) diff --git a/examples/animated_demo_live.exs b/examples/animated_demo_live.exs index 28c72f2..aa51d5c 100644 --- a/examples/animated_demo_live.exs +++ b/examples/animated_demo_live.exs @@ -86,19 +86,13 @@ defmodule AnimatedDemoLive do def render(assigns) do ~H""" <.graph dimensions={@dimensions}> - + {value} - + {Calendar.strftime(datetime, "%-I:%M:%S")} diff --git a/examples/demo_live.exs b/examples/demo_live.exs index c39a754..627a1e4 100644 --- a/examples/demo_live.exs +++ b/examples/demo_live.exs @@ -73,7 +73,7 @@ defmodule DemoLive do <.graph dimensions={@dimensions}> <%!-- X-axis labels --%> - + {Calendar.strftime(date, "%-m/%-d")} @@ -91,7 +91,7 @@ defmodule DemoLive do <%!-- Y-axis labels --%> - + {value} diff --git a/lib/plox/helpers/axis.ex b/lib/plox/helpers/axis.ex index dbc2a5f..4d00907 100644 --- a/lib/plox/helpers/axis.ex +++ b/lib/plox/helpers/axis.ex @@ -15,19 +15,18 @@ defmodule Plox.Helpers.Axis do @doc """ Renders multiple labels below or above the graph, along the given - `axis`. Defaults to below. + `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, :any, required: true - attr :dimensions, :any, required: true + 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`)" @@ -42,7 +41,7 @@ defmodule Plox.Helpers.Axis do ` elements directly for that purpose. ## Examples - + {value} """ - attr :axis, :any, required: true - attr :dimensions, :any, required: true + 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`)" @@ -95,7 +93,7 @@ defmodule Plox.Helpers.Axis do ~H""" Date: Wed, 21 Jan 2026 09:31:50 -0800 Subject: [PATCH 10/16] Update README and migration guide to use new Axis and Grid helpers --- README.md | 12 ++++++------ docs/migration_guide.md | 24 +++++++++++++++++------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 451d1aa..a40fd5d 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..b787806 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} /> From 7479edde19475f08a4a7207b19e283d8ba18ab2e Mon Sep 17 00:00:00 2001 From: Nikki Kyllonen Date: Wed, 21 Jan 2026 09:36:49 -0800 Subject: [PATCH 11/16] Remove old label and grid line components --- lib/plox.ex | 252 ---------------------------------------------------- 1 file changed, 252 deletions(-) diff --git a/lib/plox.ex b/lib/plox.ex index 5f23159..9a69695 100644 --- a/lib/plox.ex +++ b/lib/plox.ex @@ -8,8 +8,6 @@ defmodule Plox do alias Plox.Constants alias Plox.Dimensions alias Plox.Scale - alias Plox.XAxis - alias Plox.YAxis @doc """ Entrypoint component for rendering graphs and plots. @@ -36,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) ++ Constants.svg_presentation_attrs() - - 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: "middle" - attr :rest, :global, include: Constants.svg_presentation_attrs() - - 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) ++ Constants.svg_presentation_attrs() - - 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: Constants.svg_presentation_attrs() - - 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: Constants.svg_presentation_attrs() - - 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: Constants.svg_presentation_attrs() - - 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: Constants.svg_presentation_attrs() - - 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: Constants.svg_presentation_attrs() - - def y_axis_grid_line(assigns) do - ~H""" - - """ - end - @doc """ Draws a SVG `` element connecting a series of points. """ From 5ecdd91f422c75dbd9718ab025734d61bfa27101 Mon Sep 17 00:00:00 2001 From: Nikki Kyllonen Date: Wed, 21 Jan 2026 09:59:48 -0800 Subject: [PATCH 12/16] Add dominant-baseline and text-anchor attrs so that they are customizable --- lib/plox/helpers/axis.ex | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/plox/helpers/axis.ex b/lib/plox/helpers/axis.ex index 4d00907..69ff5b8 100644 --- a/lib/plox/helpers/axis.ex +++ b/lib/plox/helpers/axis.ex @@ -32,6 +32,8 @@ defmodule Plox.Helpers.Axis do 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 @@ -42,8 +44,8 @@ defmodule Plox.Helpers.Axis do :for={value <- scale_values(@axis, Map.take(assigns, [:ticks, :step, :start]))} x={@axis[value]} y={below_graph(@axis.dimensions, @gap)} - text-anchor="middle" - dominant-baseline="hanging" + dominant-baseline={assigns[:"dominant-baseline"] || "hanging"} + text-anchor={assigns[:"text-anchor"]} {@rest} > {render_slot(@inner_block, value)} @@ -57,8 +59,8 @@ defmodule Plox.Helpers.Axis do :for={value <- scale_values(@axis, Map.take(assigns, [:ticks, :step, :start]))} x={@axis[value]} y={above_graph(@axis.dimensions, @gap)} - text-anchor="middle" - dominant-baseline="text-bottom" + dominant-baseline={assigns[:"dominant-baseline"] || "text-bottom"} + text-anchor={assigns[:"text-anchor"]} {@rest} > {render_slot(@inner_block, value)} @@ -85,6 +87,8 @@ defmodule Plox.Helpers.Axis do 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 @@ -95,8 +99,8 @@ defmodule Plox.Helpers.Axis do :for={value <- scale_values(@axis, Map.take(assigns, [:ticks, :step, :start]))} x={left_of_graph(@axis.dimensions, @gap)} y={@axis[value]} - text-anchor="end" - dominant-baseline="middle" + dominant-baseline={assigns[:"dominant-baseline"]} + text-anchor={assigns[:"text-anchor"] || "end"} {@rest} > {render_slot(@inner_block, value)} @@ -110,8 +114,8 @@ defmodule Plox.Helpers.Axis do :for={value <- scale_values(@axis, Map.take(assigns, [:ticks, :step, :start]))} x={right_of_graph(@axis.dimensions, @gap)} y={@axis[value]} - text-anchor="start" - dominant-baseline="middle" + dominant-baseline={assigns[:"dominant-baseline"]} + text-anchor={assigns[:"text-anchor"] || "start"} {@rest} > {render_slot(@inner_block, value)} From a16c0b3280b9b5ea0385ef64d0244535df45553b Mon Sep 17 00:00:00 2001 From: "Nikki (she/her)" Date: Thu, 22 Jan 2026 15:46:53 -0800 Subject: [PATCH 13/16] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 4 ++-- docs/migration_guide.md | 2 +- lib/plox/helpers/grid.ex | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a40fd5d..4bb6c48 100644 --- a/README.md +++ b/README.md @@ -66,8 +66,8 @@ Once you have those, you can render a `graph` component within your HEEx templat {value} - - + + <.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 b787806..62ba8ed 100644 --- a/docs/migration_guide.md +++ b/docs/migration_guide.md @@ -105,7 +105,7 @@ dataset = dimensions={@dimensions} stroke="#D3D3D3" /> - + Date: Thu, 22 Jan 2026 16:11:44 -0800 Subject: [PATCH 14/16] Test scale_values/2 delegates to Scale.values/2 --- test/plox_test.exs | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/test/plox_test.exs b/test/plox_test.exs index 94eebac..cc656cf 100644 --- a/test/plox_test.exs +++ b/test/plox_test.exs @@ -50,4 +50,43 @@ 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 end From 6038dbc23a36956cbc4194a16880352dc7044159 Mon Sep 17 00:00:00 2001 From: Nikki Kyllonen Date: Thu, 22 Jan 2026 18:03:12 -0800 Subject: [PATCH 15/16] Update positional helpers to include padding + Add tests --- lib/plox.ex | 16 ++++++------- test/plox_test.exs | 60 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 8 deletions(-) diff --git a/lib/plox.ex b/lib/plox.ex index 9a69695..8cac437 100644 --- a/lib/plox.ex +++ b/lib/plox.ex @@ -228,7 +228,7 @@ defmodule Plox do See `Plox.Constants.default_label_gap/0` for default gap value. """ def above_graph(dimensions, gap \\ Constants.default_label_gap()) do - dimensions.margin.top - gap + dimensions.margin.top + dimensions.padding.top - gap end @doc """ @@ -236,7 +236,7 @@ defmodule Plox do 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 + gap + dimensions.height - dimensions.margin.bottom - dimensions.padding.bottom + gap end @doc """ @@ -244,7 +244,7 @@ defmodule Plox do 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 - gap + dimensions.margin.left + dimensions.padding.left - gap end @doc """ @@ -252,28 +252,28 @@ defmodule Plox do 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 + gap + 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 + 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 + 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 + 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 + def graph_right(dimensions), do: dimensions.width - dimensions.margin.right - dimensions.padding.right # @doc """ # Bar plot. diff --git a/test/plox_test.exs b/test/plox_test.exs index cc656cf..fdd1f93 100644 --- a/test/plox_test.exs +++ b/test/plox_test.exs @@ -89,4 +89,64 @@ defmodule PloxTest do Plox.Scale.values(scale, %{start: ~N[2019-01-01 00:01:00]}) end end + + describe "positional helper functions" do + setup do + dimensions = Plox.Dimensions.new(800, 600, margin: {50, 40, 60, 70}, padding: {10, 20, 30, 40}) + + %{dimensions: dimensions} + end + + test "above_graph/1 returns y-coordinate above graph with default gap", %{dimensions: dimensions} do + expected = 50 + 10 - Plox.Constants.default_label_gap() + assert Plox.above_graph(dimensions) == expected + end + + test "above_graph/2 returns y-coordinate above graph with custom gap", %{dimensions: dimensions} do + assert Plox.above_graph(dimensions, 20) == 50 + 10 - 20 + end + + test "below_graph/1 returns y-coordinate below graph with default gap", %{dimensions: dimensions} do + expected = 600 - 60 - 30 + Plox.Constants.default_label_gap() + assert Plox.below_graph(dimensions) == expected + end + + test "below_graph/2 returns y-coordinate below graph with custom gap", %{dimensions: dimensions} do + assert Plox.below_graph(dimensions, 15) == 600 - 60 - 30 + 15 + end + + test "left_of_graph/1 returns x-coordinate left of graph with default gap", %{dimensions: dimensions} do + expected = 70 + 40 - Plox.Constants.default_label_gap() + assert Plox.left_of_graph(dimensions) == expected + end + + test "left_of_graph/2 returns x-coordinate left of graph with custom gap", %{dimensions: dimensions} do + assert Plox.left_of_graph(dimensions, 10) == 70 + 40 - 10 + end + + test "right_of_graph/1 returns x-coordinate right of graph with default gap", %{dimensions: dimensions} do + expected = 800 - 40 - 20 + Plox.Constants.default_label_gap() + assert Plox.right_of_graph(dimensions) == expected + end + + test "right_of_graph/2 returns x-coordinate right of graph with custom gap", %{dimensions: dimensions} do + assert Plox.right_of_graph(dimensions, 20) == 800 - 40 - 20 + 20 + end + + test "graph_top/1 returns top boundary of graph area", %{dimensions: dimensions} do + assert Plox.graph_top(dimensions) == 50 + 10 + end + + test "graph_bottom/1 returns bottom boundary of graph area", %{dimensions: dimensions} do + assert Plox.graph_bottom(dimensions) == 600 - 60 - 30 + end + + test "graph_left/1 returns left boundary of graph area", %{dimensions: dimensions} do + assert Plox.graph_left(dimensions) == 70 + 40 + end + + test "graph_right/1 returns right boundary of graph area", %{dimensions: dimensions} do + assert Plox.graph_right(dimensions) == 800 - 40 - 20 + end + end end From 1893ef2df76ba8535294738318dc497495472141 Mon Sep 17 00:00:00 2001 From: Nikki Kyllonen Date: Fri, 23 Jan 2026 10:18:32 -0800 Subject: [PATCH 16/16] Make positional helper tests easier to understand --- test/plox_test.exs | 106 ++++++++++++++++++++++++++++++++------------- 1 file changed, 76 insertions(+), 30 deletions(-) diff --git a/test/plox_test.exs b/test/plox_test.exs index fdd1f93..349040a 100644 --- a/test/plox_test.exs +++ b/test/plox_test.exs @@ -92,61 +92,107 @@ defmodule PloxTest do describe "positional helper functions" do setup do - dimensions = Plox.Dimensions.new(800, 600, margin: {50, 40, 60, 70}, padding: {10, 20, 30, 40}) + margins = {50, 40, 60, 70} + paddings = {10, 20, 30, 40} + dimensions = Plox.Dimensions.new(800, 600, margin: margins, padding: paddings) - %{dimensions: dimensions} + %{dimensions: dimensions, margins: margins, paddings: paddings} end - test "above_graph/1 returns y-coordinate above graph with default gap", %{dimensions: dimensions} do - expected = 50 + 10 - Plox.Constants.default_label_gap() - assert Plox.above_graph(dimensions) == expected + 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} do - assert Plox.above_graph(dimensions, 20) == 50 + 10 - 20 + 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} do - expected = 600 - 60 - 30 + Plox.Constants.default_label_gap() - assert Plox.below_graph(dimensions) == expected + 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} do - assert Plox.below_graph(dimensions, 15) == 600 - 60 - 30 + 15 + 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} do - expected = 70 + 40 - Plox.Constants.default_label_gap() - assert Plox.left_of_graph(dimensions) == expected + 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} do - assert Plox.left_of_graph(dimensions, 10) == 70 + 40 - 10 + 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} do - expected = 800 - 40 - 20 + Plox.Constants.default_label_gap() - assert Plox.right_of_graph(dimensions) == expected + 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} do - assert Plox.right_of_graph(dimensions, 20) == 800 - 40 - 20 + 20 + 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} do - assert Plox.graph_top(dimensions) == 50 + 10 + 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} do - assert Plox.graph_bottom(dimensions) == 600 - 60 - 30 + 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} do - assert Plox.graph_left(dimensions) == 70 + 40 + 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} do - assert Plox.graph_right(dimensions) == 800 - 40 - 20 + 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