DEV Community

Ted Petrou
Ted Petrou

Posted on • Originally published at Medium on

Recreate the Tesla Cybertruck in Matplotlib — Solution to Dunder Data Challenge #6

This is a Tesla Cybertruck created using Matplotlib

Recreate the Tesla Cybertruck in Matplotlib — Solution to Dunder Data Challenge #6

In this challenge, you will recreate the new Tesla Cybertruck unveiled last week using matplotlib. Our goal is to recreate this image below.

Begin Mastering Data Science Now for Free!

Take my free Intro to Pandas course to begin your journey mastering data analysis with Python.

Solution

Click the video below to view the animation and final solution.

Tutorial

A tutorial will now follow that describes the recreation. It will discuss the following:

  • Figure and Axes setup
  • Adding shapes
  • Color gradients
  • Animation

Understanding these topics should give you enough to start animating your own figures in matplotlib.

Figure and Axes setup

We first create a matplotlib Figure and Axes, remove the axis labels and tick marks, and set the x and y axis limits. The fill_between method is used to set two different background colors.

import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
fig, ax = plt.subplots(figsize=(16, 8))
ax.axis('off')
ax.set\_ylim(0, 1)
ax.set\_xlim(0, 2)
ax.fill\_between(x=[0, 2], y1=.36, y2=1, color='black')
ax.fill\_between(x=[0, 2], y1=0, y2=.36, color='#101115');
Enter fullscreen mode Exit fullscreen mode

Shapes in matplotlib

Most of the Cybertruck is composed of shapes (patches in matplotlib terminology) — circles, rectangles, and polygons. These shapes are available in the patches matplotlib module. After importing, we instantiate single instances of these patches and then call the add_patch method to add the patch to the Axes.

For the Cybertruck, I used three patches, Polygon, Rectangle, and Circle. They each have different parameters available in their constructor. I first constructed the body of the car as four polygons. Two other polygons were used for the rims. Each polygon is provided a list of x, y coordinates where the corners are located. Matplotlib connects all the points in order and fills it in with the provided color.

from matplotlib.patches import Polygon, Rectangle, Circle
top = Polygon([[.62, .51], [1, .66], [1.6, .56]], color='#DCDCDC')
windows = Polygon([[.74, .54], [1, .64], [1.26, .6], [1.262, .57]],  
                   color='black')
windows\_bottom = Polygon([[.8, .56], [1, .635], [1.255, .597], 
                          [1.255, .585]], color='#474747')
base = Polygon([[.62, .51], [.62, .445], [.67, .5], [.78, .5], 
                [.84, .42], [1.3, .423],[1.36, .51], [1.44, .51],  
                [1.52, .43], [1.58, .44], [1.6, .56]], 
                color="#1E2329")
left\_rim = Polygon([[.62, .445], [.67, .5], [.78, .5], [.84, .42], 
                    [.824, .42], [.77, .49],[.674, .49], 
                    [.633, .445]], color='#373E48')
right\_rim = Polygon([[1.3, .423], [1.36, .51], [1.44, .51], 
                     [1.52, .43], [1.504, .43], [1.436, .498], 
                     [1.364, .498], [1.312, .423]], color='#4D586A')

ax.add\_patch(top)
ax.add\_patch(windows)
ax.add\_patch(windows\_bottom)
ax.add\_patch(base)
ax.add\_patch(left\_rim)
ax.add\_patch(right\_rim)
fig
Enter fullscreen mode Exit fullscreen mode

Learn more

I have free courses to get started learning data analysis using Python at Dunder Data. I am the author of the following books:

Tires

I used three Circle patches for each of the tires. You must provide the center and radius.

left\_tire = Circle((.724, .39), radius=.075, color="#202328")
right\_tire = Circle((1.404, .39), radius=.075, color="#202328")
left\_inner\_tire = Circle((.724, .39), radius=.052, color="#15191C")
right\_inner\_tire = Circle((1.404, .39), radius=.052, 
                          color="#15191C")
left\_spoke = Circle((.724, .39), radius=.019, color="#202328")
right\_spoke = Circle((1.404, .39), radius=.019, color="#202328")
left\_inner\_spoke = Circle((.724, .39), radius=.011, color="#131418")
right\_inner\_spoke = Circle((1.404, .39), radius=.011, color="#131418")

ax.add\_patch(left\_tire)
ax.add\_patch(right\_tire)
ax.add\_patch(left\_inner\_tire)
ax.add\_patch(right\_inner\_tire)
ax.add\_patch(left\_spoke)
ax.add\_patch(right\_spoke)
ax.add\_patch(left\_inner\_spoke)
ax.add\_patch(right\_inner\_spoke)
fig
Enter fullscreen mode Exit fullscreen mode

Axels

I used the Rectangle patch to represent the two 'axels' (this isn't the correct term, but you'll see what I mean) going through the tires. You must provide a coordinate for the lower left corner and a width and a height. You can also provide it an angle (in degrees) to further control its position. Notice that they are currently displayed over the inner tire circle. This will get fixed when we combine all the patches together.

left\_left\_axel = Rectangle((.687, .427), width=.104, height=.005, 
                           angle=315, color='#202328')
left\_right\_axel = Rectangle((.761, .427), width=.104, height=.005,  
                            angle=225, color='#202328')
right\_left\_axel = Rectangle((1.367, .427), width=.104, height=.005, 
                            angle=315, color='#202328')
right\_right\_axel = Rectangle((1.441, .427), width=.104, height=.005, 
                             angle=225, color='#202328')
ax.add\_patch(left\_left\_axel)
ax.add\_patch(left\_right\_axel)
ax.add\_patch(right\_left\_axel)
ax.add\_patch(right\_right\_axel)
fig
Enter fullscreen mode Exit fullscreen mode

Other details

The front bumper, head light, tail light, door and window lines are added below. I used regular matplotlib lines for some of these. Those lines are not patches and get added to the Axes without any other additional method.

# other details
front = Polygon([[.62, .51], [.597, .51], [.589, .5], [.589, .445], 
                 [.62, .445]], color='#26272d')
front\_bottom = Polygon([[.62, .438], [.58, .438], [.58, .423], [.62, 
                         .423]], color='#26272d')
head\_light = Polygon([[.62, .51], [.597, .51], [.589, .5], [.589, 
                      .5], [.62, .5]], color='aqua')
step = Polygon([[.84, .39], [.84, .394], [1.3, .397], [1.3, .393]], 
                color='#1E2329')
# doors
ax.plot([.84, .84], [.42, .523], color='black', lw=.5)
ax.plot([1.02, 1.04], [.42, .53], color='black', lw=.5)
ax.plot([1.26, 1.26], [.42, .54], color='black', lw=.5)

ax.plot([.84, .85], [.523, .547], color='black', lw=.5)
ax.plot([1.04, 1.04], [.53, .557], color='black', lw=.5)
ax.plot([1.26, 1.26], [.54, .57], color='black', lw=.5)

# window lines
ax.plot([.87, .88], [.56, .59], color='black', lw=1)
ax.plot([1.03, 1.04], [.56, .63], color='black', lw=.5)

# tail light
tail\_light = Circle((1.6, .56), radius=.007, color='red', alpha=.6)
tail\_light\_center = Circle((1.6, .56), radius=.003, color='yellow', 
                           alpha=.6)
tail\_light\_up = Polygon([[1.597, .56], [1.6, .6], [1.603, .56]], 
                         color='red', alpha=.4)
tail\_light\_right = Polygon([[1.6, .563], [1.64, .56], [1.6, .557]], 
                            color='red', alpha=.4)
tail\_light\_down = Polygon([[1.597, .56], [1.6, .52], [1.603, .56]], 
                           color='red', alpha=.4)

ax.add\_patch(front)
ax.add\_patch(front\_bottom)
ax.add\_patch(head\_light)
ax.add\_patch(step)
ax.add\_patch(tail\_light)
ax.add\_patch(tail\_light\_center)
ax.add\_patch(tail\_light\_up)
ax.add\_patch(tail\_light\_right)
ax.add\_patch(tail\_light\_down)
fig
Enter fullscreen mode Exit fullscreen mode

Color gradients for the head light beam

The head light beam has a distinct color gradient that dissipates into the night sky. This is a challenging to do. I found an excellent answer on Stack Overflow from user Joe Kington on how to do this. We begin, by using the imshow function which creates images from 3-dimensional arrays. We create a 100 x 100 x 4 array that represents 100 x 100 points of RGBA (red, green, blue, alpha) values. Every point has the same red, green, and blue values of (0, 1, 1) which represents the color alpha, but has an alpha value that ranges from 0 to 1. The further from the head light, the smaller the alpha. The alpha represents opacity. This slowly decreases the opacity until it reaches 0. The extent parameter controls the rectangular region (xmin, xmax, ymin, ymax) where the image will be shown.

import matplotlib.colors as mcolors
z = np.empty((100, 100, 4), dtype=float)
rgb = mcolors.colorConverter.to\_rgb('aqua')
z[:, :, :3] = rgb
alphas = np.linspace(0, 1, 100)[:, None]
alphas = np.tile(alphas, 100).T
z[:, :, -1] = alphas

im = ax.imshow(z, extent=[.3, .589, .501, .505], zorder=1)
fig
Enter fullscreen mode Exit fullscreen mode

Proportional alphas

Below, two more aqua images are added. The alpha values are directly proportional to the distance from the center of the rectangular region defined by extent. They eventually decrease to 0.

# beam clouds
x = np.arange(100)
y = (50 - x) \*\* 2
z2 = np.empty((100, 100, 4), dtype=float)
rgb = mcolors.colorConverter.to\_rgb('aqua')
z2[:,:,:3] = rgb
z2[:, :, -1] = 1 - np.sqrt(y.reshape(1, -1) + y.reshape(-1, 1)) / 71
z2[:, :, -1] \*\*= 2
z2[:, :, -1] \*= .7

im2 = ax.imshow(z2, extent=[.55, .65, .45, .55], zorder=1)
im3 = ax.imshow(z2, extent=[.38, .58, .45, .55], zorder=1)
fig
Enter fullscreen mode Exit fullscreen mode

Gradients in patches

Color gradients within patches is a bit more work. You’ll need to first create an image like we did above. Construct the patch setting the color to ‘none’ and add the patch as normal. Finally, call the set_clip_path method of the image passing it the patch.

beam\_cloud\_1 = Polygon([[.6, .45], [.57, .47], [.55, .5], 
                        [.57, .53], [.6, .55], [.62, .54], 
                        [.625, .52], [.63, .5], [.625, .48], 
                        [.62, .46] ], color='none')

beam\_cloud\_2 = Polygon([[.58, .5], [.52, .53], [.48, .54], 
                        [.44, .53], [.38, .5], [.44, .47], 
                        [.5, .46], [.52, .47]], color='none')

ax.add\_patch(beam\_cloud\_1)
im2.set\_clip\_path(beam\_cloud\_1)

ax.add\_patch(beam\_cloud\_2)
im3.set\_clip\_path(beam\_cloud\_2)
fig
Enter fullscreen mode Exit fullscreen mode

Creating a Function to Draw the Car

All of our work from above can be placed in a function that draws the car. This will be used when initializing our animation. The function is not shown, but just puts all of the above code in a single code block.

Animation

Animation in matplotlib is fairly straightforward. You must create a function that updates the position of the objects in your figure for every frame. This function is called repeatedly for each frame that you create.

In the update function below, we loop through each patch, line and image in our Axes and reduce the x-value of each plotted object by .015. This has the effect of moving the truck to the left. The trickiest part was changing the x and y values for the rectangular tire ‘axels’. Some basic trigonemtry helps calculate this.

Finally, the FuncAnimation class from the animation module is used to construct the animation. We provide it our update function and a function to call once to initialize the figure along with the number of frames and any extra arguments used during update.

def update(frame\_number, x\_delta, radius, angle):
    if frame\_number == 0:
        return
    ax = fig.axes[0]
    for patch in ax.patches:
        if isinstance(patch, Polygon):
            arr = patch.get\_xy()
            arr[:, 0] -= x\_delta
        elif isinstance(patch, Circle):
            x, y = patch.get\_center()
            patch.set\_center((x - x\_delta, y))
        elif isinstance(patch, Rectangle):
            xd\_old = -np.cos(np.pi \* patch.angle / 180) \* radius
            yd\_old = -np.sin(np.pi \* patch.angle / 180) \* radius
            patch.angle += angle
            xd = -np.cos(np.pi \* patch.angle / 180) \* radius
            yd = -np.sin(np.pi \* patch.angle / 180) \* radius
            x = patch.get\_x()
            y = patch.get\_y()
            x\_new = x - x\_delta + xd - xd\_old
            y\_new = y + yd - yd\_old
            patch.set\_x(x\_new)
            patch.set\_y(y\_new)

    for line in ax.lines:
        xdata = line.get\_xdata()
        line.set\_xdata(xdata - x\_delta)

    for image in ax.images:
        extent = image.get\_extent()
        extent[0] -= x\_delta
        extent[1] -= x\_delta

animation = FuncAnimation(fig, update, init\_func=draw\_car, 
                          frames=110, repeat=False, 
                          fargs=(.015, .052, 4))
Enter fullscreen mode Exit fullscreen mode

Save animation

Finally, we can save the animation as an mp4 file (assuming you have ffmpeg). The html video tag is used in the markdown to display the animation.

animation.save('../images/tesla\_animate.mp4', fps=30, bitrate=3000)
Enter fullscreen mode Exit fullscreen mode

Animation on YouTube

Here is the final animation on YouTube

Master Python, Data Science and Machine Learning

Immerse yourself in my comprehensive path for mastering data science and machine learning with Python. Purchase the All Access Pass to get lifetime access to all current and future courses . Some of the courses it contains:

Get the All Access Pass now!


Top comments (0)