Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Fun with Graphs

graphs.livemd

Fun with Graphs

Lets checkout the graphing functionality of LiveBooks

Lets draw some graphs, first setup the graphing tools.

Mix.install([
  {:vega_lite, "~> 0.1.0"},
  {:kino, "~> 0.2.0"},
  {:math, "~> 0.6.0"}
])

alias VegaLite, as: Vl

Lets generate a parabola

$$ y = x^2$$

require Math

data =
  -10..10
  |> Enum.map(fn x -> %{"x" => x, "y" => Math.pow(x, 2)} end)
Vl.new(width: 400, height: 300)
|> Vl.data_from_values(data)
|> Vl.mark(:point)
|> Vl.encode_field(:x, "x", type: :quantitative)
|> Vl.encode_field(:y, "y", type: :quantitative)

Lets try an exponential

$$e^x$$

data =
  0..10
  |> Enum.map(fn x -> %{"x" => x, "y" => Math.exp(x)} end)
Vl.new(width: 400, height: 300)
|> Vl.data_from_values(data)
|> Vl.mark(:line)
|> Vl.encode_field(:x, "x", type: :quantitative)
|> Vl.encode_field(:y, "y", type: :quantitative)

Lets try an input - enter a power of x and see how it graphs

pow = IO.gets("pow") |> String.trim() |> String.to_integer()

IO.inspect("Power: #{pow}")

data =
  -10..10
  |> Enum.map(fn x -> %{"x" => x, "y" => Math.pow(x, pow)} end)

Vl.new(width: 400, height: 300)
|> Vl.data_from_values(data)
|> Vl.mark(:point)
|> Vl.encode_field(:x, "x", type: :quantitative)
|> Vl.encode_field(:y, "y", type: :quantitative)

Plotting multiple lines - towards a Epicycloid

> ​If we roll a circle around the circumference of another circle, the shape traced by a point on the moving circle is an epicycloid.

See Draw Curves with Straight Lines

Lets try drawing multiple lines on the same graph

limit = 10

dataA =
  0..limit
  |> Enum.flat_map(fn x ->
    [
      %{"x" => 0, "y" => limit - x, "n" => "lower_#{x}"},
      %{"x" => x, "y" => 0, "n" => "lower_#{x}"}
    ]
  end)

dataB =
  0..limit
  |> Enum.flat_map(fn x ->
    [
      %{"x" => limit, "y" => limit - x, "n" => "upper_#{x}"},
      %{"x" => x, "y" => limit, "n" => "upper_#{x}"}
    ]
  end)

Vl.new(width: 400, height: 300)
|> Vl.data(sequence: [start: 0, stop: 10, step: 1, as: "x"])
|> Vl.data_from_values(dataA ++ dataB)
|> Vl.mark(:line)
|> Vl.encode_field(:x, "x", type: :quantitative)
|> Vl.encode_field(:y, "y", type: :quantitative)
|> Vl.encode_field(:color, "n", type: :nominal)

What we want to do now, is to be able to draw a circle with numbers, so that we can draw a line from number 22 to 64 and this will draw a cord across the circle.

As an example, think of a clock, drawing from 3 to 9 would be a horizontal line, where as 11 to 5 would be at an angle.

The challenge to is draw a circle of n divisions and map that location to a point on the circle.

Lets try drawing a clock face.

We will need some math helpers. We need to calculate the x and y positions for each number on the clock face using the following formula

$$x = centre_x + r cos(a)$$ $$y = centre_y + r sin(a)$$

The a is in radians (so we will need to convert between degrees and radians)

defmodule MathHelpers do
  @radius 200

  def to_radians(0) do
    0
  end

  def to_radians(x, number_of_points_to_draw) do
    point_to_angle(x, number_of_points_to_draw) * (Math.pi() / 180)
  end

  def point_to_angle(0, _number_of_points_to_draw) do
    0
  end

  def point_to_angle(x, number_of_points_to_draw) do
    360 / number_of_points_to_draw * x
  end

  # See https://stackoverflow.com/questions/839899/how-do-i-calculate-a-point-on-a-circle-s-circumference
  def circle_x(cx, x, number_of_points_to_draw) do
    cx + @radius * Math.sin(to_radians(x, number_of_points_to_draw))
  end

  def circle_y(cy, x, number_of_points_to_draw) do
    cy + @radius * Math.cos(to_radians(x, number_of_points_to_draw))
  end

  def coords_for_point(n, number_of_points_to_draw, {cx, cy}, iter) do
    %{
      "x" => circle_x(cx, n, number_of_points_to_draw),
      "y" => circle_y(cy, n, number_of_points_to_draw),
      "n" => n,
      "iter" => iter
    }
  end
end
w = 400
h = 400
# Center of the circle
center = {w / 2, h / 2}
# Split the circle into x divisions - so draw n points around the circumference
number_of_points_to_draw = 12

circle =
  0..number_of_points_to_draw
  |> Enum.map(fn n ->
    MathHelpers.coords_for_point(n, number_of_points_to_draw, center, "circle")
  end)

Vl.new(width: w, height: h)
|> Vl.data_from_values(circle)
|> Vl.mark(:point)
|> Vl.encode_field(:x, "x", type: :quantitative)
|> Vl.encode_field(:y, "y", type: :quantitative)

Lets try drawing lines between random points on the circle

w = 400
h = 400
# Center of the circle
center = {w / 2, h / 2}
# Split the circle into x divisions - so draw n points around the circumference
number_of_points_to_draw = 12
number_of_lines = 150

circle =
  0..number_of_points_to_draw
  |> Enum.map(fn n ->
    MathHelpers.coords_for_point(n, number_of_points_to_draw, center, "circle")
  end)

lines =
  1..number_of_lines
  |> Enum.flat_map(fn x ->
    [
      MathHelpers.coords_for_point(
        Enum.random(0..number_of_points_to_draw),
        number_of_points_to_draw,
        center,
        x
      ),
      MathHelpers.coords_for_point(
        Enum.random(0..number_of_points_to_draw),
        number_of_points_to_draw,
        center,
        x
      )
    ]
  end)

# Draw it
Vl.new(width: w, height: h)
|> Vl.layers([
  Vl.new()
  |> Vl.data_from_values(circle)
  |> Vl.mark(:point)
  |> Vl.encode_field(:x, "x", type: :quantitative)
  |> Vl.encode_field(:y, "y", type: :quantitative),
  Vl.new()
  |> Vl.data_from_values(lines)
  |> Vl.mark(:line)
  |> Vl.encode_field(:x, "x", type: :quantitative)
  |> Vl.encode_field(:y, "y", type: :quantitative)
  |> Vl.encode_field(:color, "iter", type: :nominal)
])

The last part of the puzzle is to calculate where the lines should go from / to.

> For a cardioid, join each point on the circle to the point 2 x its number value with a straight line (join 1 to 2, 2 to 4, 3 to 6 and so on).
> There are 100 points on the circle. When you get to point 51, which maps to 102, keep going round the circle, subtracting 100 from the number you need, to find the correct point.

Draw Curves with Straight Lines

defmodule Epicycloid do
  def cardioid(0, _limit) do
    0
  end

  def cardioid(n, limit) when 2 * n <= limit do
    2 * n
  end

  def cardioid(n, limit) do
    cardioid(n - limit, limit)
  end
end
limit = 100
Epicycloid.cardioid(2, limit) |> IO.inspect()
Epicycloid.cardioid(3, limit) |> IO.inspect()
Epicycloid.cardioid(102, limit) |> IO.inspect()
w = 400
h = 400
# Center of the circle
center = {w / 2, h / 2}
number_of_points_to_draw = 100
number_of_lines = 150

circle =
  0..number_of_points_to_draw
  |> Enum.map(fn n ->
    MathHelpers.coords_for_point(n, number_of_points_to_draw, center, "circle")
  end)

lines =
  1..number_of_lines
  |> Enum.flat_map(fn x ->
    [
      MathHelpers.coords_for_point(x, number_of_points_to_draw, center, x),
      MathHelpers.coords_for_point(
        Epicycloid.cardioid(x, number_of_points_to_draw),
        number_of_points_to_draw,
        center,
        x
      )
    ]
  end)

# Draw it
Vl.new(width: w, height: h)
|> Vl.layers([
  Vl.new()
  |> Vl.data_from_values(circle)
  |> Vl.mark(:point)
  |> Vl.encode_field(:x, "x", type: :quantitative)
  |> Vl.encode_field(:y, "y", type: :quantitative),
  Vl.new()
  |> Vl.data_from_values(lines)
  |> Vl.mark(:line)
  |> Vl.encode_field(:x, "x", type: :quantitative)
  |> Vl.encode_field(:y, "y", type: :quantitative)
  |> Vl.encode_field(:color, "iter", type: :nominal)
])