Skip to content

Commit

Permalink
Add Tucan.Geometry.ellipse/5
Browse files Browse the repository at this point in the history
  • Loading branch information
pnezis committed Mar 20, 2024
1 parent 86ab42a commit b844e62
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 2 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ Tucan.Finance.candlestick(:ohlc, "date", "open", "high", "low", "close",
)
```

- Add `Tucan.Geometry.ellipse/5`

```tucan
Tucan.layers([
Tucan.Geometry.ellipse({0, 0}, 5, 3, 0, line_color: "green"),
Tucan.Geometry.ellipse({2, 2}, 4, 1, 40, line_color: "red"),
])
|> Tucan.Scale.set_xy_domain(-7, 7)
```

- Add `Tucan.Axes.set_color/3` helper.

## [v0.3.1](https://github.com/pnezis/tucan/tree/v0.3.1) (2024-01-20)
Expand Down
81 changes: 79 additions & 2 deletions lib/tucan/geometry.ex
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,6 @@ defmodule Tucan.Geometry do
@doc """
Draws a circle with the given `center` and `radius`.
The circle will be added as a new layer to the given plot `vl`.
## Options
#{Tucan.Options.docs(@circle_opts)}
Expand Down Expand Up @@ -132,6 +130,85 @@ defmodule Tucan.Geometry do
|> Vl.encode_field(:order, "theta")
end

ellipse_opts = [
stroke_width: [default: 1]
]

@ellipse_opts Tucan.Options.take!(
[
:stroke_width,
:stroke_dash,
:line_color,
:opacity,
:stroke_opacity,
:fill_color,
:fill_opacity
],
ellipse_opts
)
@ellipse_schema Tucan.Options.to_nimble_schema!(@ellipse_opts)

@doc """
Draws an ellipse with the given `center`, `x_radius`, `y_radius`, and `rotation_angle`.
`x_radius` is expected to be a positive number corresponding to the *major semi-axis* length, `y_radius`
a positive number corresponding to the *minor semi-axis* length and `rotation_angle` the ellipse's
rotation angle with respect to the *x-axis*.
## Options
#{Tucan.Options.docs(@circle_opts)}
## Examples
```tucan
Tucan.layers([
Tucan.Geometry.ellipse({0, 0}, 5, 3, 0),
Tucan.Geometry.ellipse({3, 4}, 5, 4, 45, line_color: "red"),
Tucan.Geometry.ellipse({4, 1}, 7, 2, -30, line_color: "green", stroke_width: 2)
])
|> Tucan.Scale.set_x_domain(-5, 10)
|> Tucan.Scale.set_y_domain(-5, 10)
```
"""
@spec ellipse(
center :: point(),
x_radius :: number(),
y_radius :: number(),
rotation_angle :: number(),
opts :: keyword()
) :: VegaLite.t()
def ellipse({x, y}, x_radius, y_radius, rotation_angle, opts \\ [])
when is_number(x_radius) and is_number(y_radius) and is_number(rotation_angle) and
x_radius > 0 and y_radius > 0 do
opts = NimbleOptions.validate!(opts, @ellipse_schema)

mark_opts =
opts
|> Tucan.Options.take_options(@ellipse_opts, :mark)
|> Tucan.Keyword.put_not_nil(:color, opts[:line_color])
|> Tucan.Keyword.put_not_nil(:fill, opts[:fill_color])

Vl.new()
|> Vl.data(sequence: [start: 0, stop: 361, step: 0.1, as: "theta"])
|> Vl.transform(calculate: ellipse_x(x, x_radius, y_radius, rotation_angle), as: "x")
|> Vl.transform(calculate: ellipse_y(y, x_radius, y_radius, rotation_angle), as: "y")
|> Vl.mark(:line, mark_opts)
|> Vl.encode_field(:x, "x", type: :quantitative)
|> Vl.encode_field(:y, "y", type: :quantitative)
|> Vl.encode_field(:order, "theta")
end

defp ellipse_x(x, x_radius, y_radius, angle) do
"#{x} + cos(datum.theta*PI/180) * cos(#{angle}*PI/180) * #{x_radius} -" <>
"sin(datum.theta*PI/180) * sin(#{angle}*PI/180) * #{y_radius}"
end

defp ellipse_y(y, x_radius, y_radius, angle) do
"#{y} + cos(datum.theta*PI/180) * sin(#{angle}*PI/180) * #{x_radius} +" <>
"sin(datum.theta*PI/180) * cos(#{angle}*PI/180) * #{y_radius}"
end

@doc """
Draws a rectangle defined by the given `upper_left` and `bottom_right` points.
Expand Down
57 changes: 57 additions & 0 deletions test/tucan/geometry_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,63 @@ defmodule Tucan.GeometryTest do
end
end

describe "ellipse/5" do
test "sequence for ellipse with the given center axes and angle" do
expected =
Vl.new()
|> Vl.mark(:line, stroke_width: 1, fill_opacity: 1, stroke_opacity: 1)
|> Vl.data(sequence: [start: 0, stop: 361, step: 0.1, as: "theta"])
|> Vl.transform(
calculate:
"-1 + cos(datum.theta*PI/180) * cos(32*PI/180) * 2.5 -sin(datum.theta*PI/180) * sin(32*PI/180) * 3",
as: "x"
)
|> Vl.transform(
calculate:
"3 + cos(datum.theta*PI/180) * sin(32*PI/180) * 2.5 +sin(datum.theta*PI/180) * cos(32*PI/180) * 3",
as: "y"
)
|> Vl.encode_field(:x, "x", type: :quantitative)
|> Vl.encode_field(:y, "y", type: :quantitative)
|> Vl.encode_field(:order, "theta")

assert Tucan.Geometry.ellipse({-1, 3}, 2.5, 3, 32) == expected
end

test "with custom style options" do
%VegaLite{spec: ellipse} =
Tucan.Geometry.ellipse({-1, 3}, 2.5, 3, -15, line_color: "red", stroke_width: 4)

assert ellipse["mark"] == %{
"strokeWidth" => 4,
"type" => "line",
"color" => "red",
"fillOpacity" => 1,
"strokeOpacity" => 1
}
end

test "with fill color and opacities" do
%VegaLite{spec: ellipse} =
Tucan.Geometry.ellipse({-1, 3}, 2.5, 3, 20,
line_color: "red",
stroke_width: 4,
fill_color: "green",
fill_opacity: 0.2,
stroke_opacity: 0.5
)

assert ellipse["mark"] == %{
"strokeWidth" => 4,
"type" => "line",
"color" => "red",
"fillOpacity" => 0.2,
"fill" => "green",
"strokeOpacity" => 0.5
}
end
end

describe "polyline/3" do
@points [{1, 2}, {5, 7}, {9, 13}, {-1, 4}]

Expand Down

0 comments on commit b844e62

Please sign in to comment.