Mathologer

The Times Tables, Mandelbrot and the Heart of Mathematics. The good old times tables lead a very exciting secret life involving the infamous Mandelbrot set.

General Imports and Functions

It's a good practice to place all the imports at the top of the document to better trace dependencies and keep them updated, and also to know which tools are required. In this case there are General Purpose imports and Jupyter specifics.

# General Purpose
#
import numpy as np
from matplotlib import pyplot as plt
from matplotlib import animation, rc
import matplotlib.lines as mlines
import colorsys
from matplotlib.collections import LineCollection

# Jupyter Specifics
#
import matplotlib as mpl
from IPython.display import HTML
from ipywidgets.widgets import interact, IntSlider, FloatSlider, Dropdown, Layout

# Some magics
#
%matplotlib inline
# nbi:hide_in

# The method (of the animation instances) to manage the
# player is controlled by the animation rc parameter.
#
# The rc parameter currently supports values of "none", "html5"
# and "jshtml".
#
#   none:    no player (display) is shown    
#   html5:   use the native HTML5 player widget
#   jshtml:  use the interactive JavaScript widget
#
# The default is none to not display a player. To display
# the native HTML5 player, # set it to "html5". For the
# interactive JavaScript widget to "jshtml".
#
rc('animation', html='html5', embed_limit='256')
# rc('animation', html='jshtml', embed_limit='512')

Basic Functions

Once everything is imported and ready to use, several functions must be defined, namely:

  1. A function to calculate the points arround a circle
  2. A function to generate each of the lines
  3. A function to plot the labels and the point in the circle
  4. A function to plot the lines in the circle

The first function is called points_arround_circle and it basically uses polar coordinates to place a given number of points arround a circle of a given radius. Here numpy is needed to make the calculation performant.

def points_arround_circle(number=100, center=(0,0), radius=1):
    theta = np.linspace(0, 2 * np.pi - (2 * np.pi / number), number)
    x = radius * np.cos(theta)
    y = radius * np.sin(theta)
    return (x, y)

Second, in order to generate the lines, the list of points is given and a new line is generated by the function get_lines_from_points.

def get_lines_from_points(x, y, factor, animated=None):
    limit = len(x)
    if animated is not None:
        for i in range(limit):
            x_range = (x[i], x[int(i * factor) % limit])
            y_range = (y[i], y[int(i * factor) % limit])
            yield mlines.Line2D(x_range, y_range)
    else:
        for i in range(limit):
            start = (x[i], y[i])
            index = int((i * factor) % limit)
            end = (x[index], y[index])          
            yield end, start

Now it's time to plot the point around the circle by plot_circle_points. Both in the circle, the points and the labels are plotted.

def plot_circle_points(x, y, ax, labels=None):
    ax.annotate("Points: {}".format(len(x)), (0.8, 0.9))
    ax.plot(x, y, "-ko", markevery=1)
    if not labels is None:
        for i, (x, y) in enumerate(zip(x, y)):
            ax.annotate(i, (x, y))

Finally, a function plot_lines which receives the axis object to plot all the lines. With the option (if given), a color for the lines in a HSV format is calculated.

def plot_lines(x, y, factor, ax, color=None):
    ax.annotate("Factor: {}".format(factor), (0.8, 1))
    lines = list(get_lines_from_points(x, y, factor))
    if color is None:
        line_segments = LineCollection(lines)
    else:
        line_segments = LineCollection(lines, colors=colorsys.hsv_to_rgb(color, 1.0, 0.8))
            
    ax.add_collection(line_segments)

Create a static plot

After all the functions needed are defined, now plotting is quite simple. Just generate the axis object and invoke the functions in the logical order, and you get the image.

One approach is manually changing the factor and points variables and then executing the plot. Since Jupyter provides support for interaction, a more user-friendly approach can be used. Change the image by moving the sliders to either side.

def plot_parametric(Factor=2, Points=100):
    
    # figsize: width|height recalculated from inches to pixels
    my_dpi=96
    plt.figure(figsize=(800/my_dpi, 800/my_dpi), dpi=my_dpi)    
    
    ax = plt.subplot()
    plt.axis('off')
    x, y = points_arround_circle(number=Points)
    plot_circle_points(x, y, ax)
    plot_lines(x, y, Factor, ax)
    plt.show()

factors = 2, 3, 4, 5, 8, 10, 16, 20, 21, 25, 26, 34
print("\nTry these Factors with different number of Points:\n", *factors, "\n")
Try these Factors with different number of Points:
 2 3 4 5 8 10 16 20 21 25 26 34 

# nbi:hide_in

interact(plot_parametric, 
        Factor=IntSlider(min=1, max=34, step=1, value=2, layout=Layout(width='90%')),
        Points=IntSlider(min=25, max=200, step=5, value=100, layout=Layout(width='90%')));
    

Construction Line by Line

The factor and the number of points is fixed for the plot by your selection, but each line is plotted per iteration. Try different factors (for the times table) and vary the number of points placed on the circle.

# nbi:hide_in

# animation function. This is called sequentially.
#
def animate_line_by_line(i, lines, ax):
    ax.add_line(next(lines))
    return []

def line_by_line(Factor, Points, Interval):
    
    # figsize: width|height recalculated from inches to pixels
    my_dpi=96    
    fig, ax = plt.subplots(figsize=(800/my_dpi, 800/my_dpi), dpi=my_dpi);
    
    plt.axis('off')
    x, y = points_arround_circle(number=Points)
    plot_circle_points(x, y, ax)
    ax.annotate("Factor: {}".format(Factor), (0.8, 1))
    ax.annotate("Delay: {}".format(Interval), (0.8, 0.8))
    lines = get_lines_from_points(x, y, Factor, animated=True)

    # call the animator. blit=True means only re-draw the parts that have changed.
    #
    anim = animation.FuncAnimation(
        fig, animate_line_by_line, frames=len(x)-2,
        interval=Interval, blit=True, fargs=(lines, ax)
    );
    plt.close()
    
    return anim
# nbi:hide_in

anim = line_by_line(Factor=2, Points=100, Interval=500)

interact(line_by_line,
  Factor=Dropdown(
    value=2, 
    options=[2, 3, 4, 5, 8, 10, 16, 20, 21, 25, 26, 34],
    description='Factor'
  ),         
  Points=Dropdown(
     value=100, 
     options=[5, 15, 25, 50, 75, 100, 150, 200],
     description='Points'
  ),
  Interval=Dropdown(
     value=150, 
     options=[300, 200, 150, 100, 75],
     description='Delay'
  )
);
# nbi:hide_in

Writer = animation.writers['ffmpeg']
writer = Writer(fps=30)

anim.save('line_by_line.mp4', writer=writer)