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.
%%HTML
<center>
<iframe
width="900" height="540"
frameborder="0" allowfullscreen
src="https://www.youtube.com/embed/qhbuKbxJsk8">
</iframe>
</center>
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')
Once everything is imported and ready to use, several functions must be defined, namely:
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)
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):
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 = 2, 3, 4, 5, 8, 10, 16, 20, 21, 25, 26, 34
print("\nTry these Factors with different number of Points:\n", *factors, "\n")
# 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%')));
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):
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("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)
The factor and the lines are fixed to construct the plotted image point-by-point, but each iteration increases the number of points. Try different factors (for the times table) and vary the number of points placed on the circle. Try different factors (for the times table) and vary the number of points placed on the circle.
# nbi:hide_in
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("Delay: {}".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, Points):
fig, ax = plt.subplots(figsize=(10, 10));
anim = animation.FuncAnimation(fig, animate_point_by_point, frames=Points, interval=Interval, blit=True, fargs=(ax, Factor, Interval));
plt.close()
return anim
# nbi:hide_in
anim = point_by_point(Factor=2, Points=100, Interval=150)
interact(point_by_point,
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=15)
anim.save('point_by_point.mp4', writer=writer)
For the animation shown in the video, the number of points on the circle is fixed. All lines are plotted simultaneously, but the factor is increased each iteration. Try different factors (for the times table) and vary the number of points placed on the circle. Try different factors (for the times table) and vary the number of points placed on the circle.
# nbi:hide_in
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("Delay: {}".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) + 1
anim = animation.FuncAnimation(fig, animate_factor_by_factor, frames=frames, interval=Interval, blit=True, fargs=(ax, Max_Points, Interval, frames));
plt.close()
return anim
# nbi:hide_in
anim = factor_by_factor(Factor=2, Max_Points=100, Interval=500)
interact(factor_by_factor,
Factor=Dropdown(
value=2,
options=[2, 3, 4, 5, 8, 10, 16, 20, 21, 25, 26, 34],
description='Factor'
),
Max_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=12)
anim.save('factor_by_factor.mp4', writer=writer)