Time Tables Visualization  Finding Patterns
After looking to a Mathologer Video about a beautiful pattern emerged from time tables, I decided to use it as a challenge and write a script to do the same with Python. Animations Included!
First, let's introduce the video I mentioned, in this video a very nice patterns emerges in something called "Times Tables"
These animations were made using Wolfram Mathematica, but in Python, there are enough tools to achieve the same.
This is the results built with Python:
Now I will examine the code used to produced the previous image and also how to create animations, there will be a link to a online notebook in case you want to experiment yourself. In this post I will show you several scenarios:
 Static Version (as the one seen above)
 Parametric Version: Where you can change with sliders the values and experiment by yourself.
 Animate Construction Line by Line: Where the factor and the number of points is fixed but each line is plot one at a time
 Animate Construction Point by Point: Where the factor and the lines are fixed but each frame increases the number of points

Animate Construction Factor by Factor: Where the lines and the number of points are fixed but the factor increases (The one shown at the end of the video), first monochrome and then with the rainbow effect seen in the video.
Note: After each of the scenarios there will be both an interactive (Jupyter Widget) and a static (image or animation) representation of the code, the interactive is built through a backend so please be patient to see the results since each time you move the slider, the full animation is recalculated. The static representation is useful for lowspeed connections and for a faster result but it can't be changed or experimented with.
Requirements:
 Jupyter: Notebook Interface
 Numpy: For array manipulation
 Matplotlib: For visualization and animation
 ffmpeg (Optional): For exporting the animation
In order to produce these images and animations, the code should be split in 4 parts:
 Import all the necesary libraris
 Define all the auxiliary functions
 Plot the animations
 Export (optional)
Initialization
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, in order to run as a script, it has to be adapted to replace the Jupyter Funtionalities
# 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, Layout
%matplotlib inline
rc('animation', html='html5')
Basic Functions
Once everything is imported and ready to use, several functions must be defined, namely:
 One function to calculate the points arround a circle
 One function to generate each of the lines
 One function to plot the labels and the point in the circle
 One 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, a different approach is needed when using this function in an animation so two sets of logic are defined inside de function.
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 and in the plot_circle_points
, both the circle, the points and the labels are ploted
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 which receives the axis object plot all the lines, with the option to use a color in a HSV format (this will be used in the final animation)
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)
Static Version
After all the functions needed are defined, now plotting a static version is quite simple, just generate the axis object and invoke the functions in the logical order and you get the image. This approach is useful to quickly experiment with a fixed number of factor and points
def plot_static(factor, points):
plt.figure(figsize=(10, 10))
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)
factor = 2
points = 100
plot_static(factor, points)
Parametric Version
One approach is to manually change the factor
and points
variables and then just execute the cell/funtion again but since Jupyter provides support for interaction, a more user friendly approach can be used through Sliders (a built in UI of IPython). Here the function plot_parametric
is exactly the same as plot_static
but it uses plt.show()
at the end to plot the image. Here the image is also static but can be change moving the sliders to either side.
def plot_parametric(Factor=2, Points=100):
plt.figure(figsize=(10, 10))
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 = [21, 29, 33, 34, 49, 51, 66, 67, 73, 76, 79, 80, 86, 91, 99]
print("Try these Factors with different number of points:", *factors)
interact(plot_parametric,
Factor=FloatSlider(min=0, max=100, step=0.1, value=2, layout=Layout(width='99%')),
Points=IntSlider(min=0, max=300, step=25, value=100, layout=Layout(width='99%')));
Animate Construction Line by Line
Now we move to animations and in this first animation both the factor and the number of points are fixed, which changes is the lines, this animations mimics the process of drawing some of theses times tables by hand and could also give some insight about the order in which the lines are plotted instead of just seen them all at once.
Animations in Matplotlib are built through an animate
function, which basically returns the objects to be printed in each frame. That's why in this animation and the following, two functions should be defined, one for the animate
API of matplotlib and the other to embed in the interact
function of IPython. Here a line_by_line
takes a given Factor
, a number of Points
and an Interval
, the first two are already familiar since we used them in the previous functions and the Interval
is just the delay between frames in miliseconds, it is tightly related to the FPS of the final animation: FPS = 1000 / delay
.
def animate_line_by_line(i, lines, ax):
ax.add_line(next(lines))
return []
def line_by_line(Factor, Points, Interval):
fig, ax = plt.subplots(figsize=(10, 10));
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("Interval: {}".format(Interval), (0.8, 0.8))
lines = get_lines_from_points(x, y, Factor, animated=True)
anim = animation.FuncAnimation(fig, animate_line_by_line, frames=len(x)2, interval=Interval, blit=True, fargs=(lines, ax));
plt.close()
return anim
interact(line_by_line,
Factor=FloatSlider(min=0, max=100, step=0.1, value=2, layout=Layout(width='99%')),
Points=IntSlider(min=1, max=200, step=1, value=100, layout=Layout(width='99%')),
Interval=IntSlider(min=5, max=500, step=5, value=75, layout=Layout(width='99%')));
Animate Construction Point by Point
Taking another perspective, maybe what's interesting isn't how the lines are plot but rather how the figure gets clearer when we add more points so in this animation the factor
is fixed and the lines are plotted all at once but each frame increases the number of points from 0 to a given number of MaxPoints
.
def animate_point_by_point(i, ax, Factor, Interval):
ax.cla()
ax.axis('off')
ax.set_ylim(1.2, 1.2)
ax.set_xlim(1.2, 1.2)
ax.annotate("Interval: {}".format(Interval), (0.8, 0.8))
x, y = points_arround_circle(number=i+1)
plot_circle_points(x, y, ax)
plot_lines(x,y,Factor, ax)
return []
def point_by_point(Factor, Interval, Max_Points):
fig, ax = plt.subplots(figsize=(10, 10));
anim = animation.FuncAnimation(fig, animate_point_by_point, frames=Max_Points, interval=Interval, blit=True, fargs=(ax, Factor, Interval));
plt.close()
return anim
interact(point_by_point,
Factor=FloatSlider(min=0, max=100, step=0.1, value=2, layout=Layout(width='99%')),
Max_Points=IntSlider(min=1, max=200, step=1, value=75, layout=Layout(width='99%')),
Interval=IntSlider(min=100, max=500, step=1, value=200, layout=Layout(width='99%')));
Animate Construction Factor by Factor
Now the animation showed in the video, which the number of points fixed and all lines are plotted at ones but the factor is increased frame by frame. When the factor is increased with a step of 1, the animation changes drastically so in for this example the factor is changed by steps of 0.1, to achieve a smoother animation. This version is monochrome, all the lines are always the same color.
def animate_factor_by_factor(i, ax, Max_Points, Interval, frames):
ax.cla()
ax.axis('off')
ax.set_ylim(1.2, 1.2)
ax.set_xlim(1.2, 1.2)
ax.annotate("Interval: {}".format(Interval), (0.8, 0.8))
x, y = points_arround_circle(number=Max_Points)
plot_circle_points(x, y, ax)
plot_lines(x, y, i / 10, ax)
return []
def factor_by_factor(Factor, Interval, Max_Points):
fig, ax = plt.subplots(figsize=(10, 10));
frames = int(Factor * 10)
anim = animation.FuncAnimation(fig, animate_factor_by_factor, frames=frames, interval=Interval, blit=True, fargs=(ax, Max_Points, Interval, frames));
plt.close()
return anim
interact(factor_by_factor,
Factor=FloatSlider(min=0, max=100, step=0.1, value=5, layout=Layout(width='99%')),
Max_Points=IntSlider(min=1, max=200, step=1, value=100, layout=Layout(width='99%')),
Interval=IntSlider(min=50, max=500, step=25, value=100, layout=Layout(width='99%')));
Animate Construction Factor by Factor with Color
Just as the previous one but with color added, in this case an additional frames
parameter is passed to the animate_factor_by_factor_colored
function and this value is the total number of frames so the HSV system is used with fixed Saturation and Value and the Hue is changing from 0 to 1 depending on the frame. To achieved this, the current frame i
is divided by the total number of frames frames
, and thus ranging from 0 to 1.
def animate_factor_by_factor_colored(i, ax, Max_Points, Interval, frames):
ax.cla()
ax.axis('off')
ax.set_ylim(1.2, 1.2)
ax.set_xlim(1.2, 1.2)
ax.annotate("Interval: {}".format(Interval), (0.8, 0.8))
x, y = points_arround_circle(number=Max_Points)
plot_circle_points(x, y, ax)
plot_lines(x, y, i / 10, ax, color=i / frames)
return []
def factor_by_factor_colored(Factor, Interval, Max_Points):
fig, ax = plt.subplots(figsize=(10, 10));
frames = int(Factor * 10)
anim = animation.FuncAnimation(fig, animate_factor_by_factor_colored, frames=frames, interval=Interval, blit=True, fargs=(ax, Max_Points, Interval, frames));
plt.close()
return anim
interact(factor_by_factor_colored,
Factor=FloatSlider(min=0, max=100, step=0.1, value=5, layout=Layout(width='99%')),
Max_Points=IntSlider(min=1, max=200, step=1, value=100, layout=Layout(width='99%')),
Interval=IntSlider(min=50, max=500, step=25, value=100, layout=Layout(width='99%')));
Export
Every animation generated can be exported as an mp4 file. It simply needs to call the function, store the result in a variable and then use the following snipped. Change Specific_function
with the one you like, and place the corresponding parameters. For instance: factor_by_factor_colored
, animate_point_by_point
anim = Specific_function(*args)
Writer = animation.writers['ffmpeg']
writer = Writer(fps=30)
anim.save('filename.mp4', writer=writer)
Notebook
Everything showed above can be executed without installing anything just by using Binder, open the gist online and experiment yourself.