This answer supplements the other, better, clearer answers here already.
Why does the Sun track out a seemingly sinusoidal path on the celestial sphere?
It seems to be sinusoidal because for low inclinations the shape is roughly to sinusoidal (straight when crossing zero, has gently curved and symmetric extrema) and so we don't stop and ask what shape it is.
Equirectangular projection maps spherical coordinates $\varphi, \theta$ or lon, lat or RA, Dec (but with zero at the equator) on to cartesian $X, Y$ axes with the mind-numbingly simple transform:
\begin{align}
X & = \varphi \\
Y & = \pi/2 - \theta, \\
\end{align}
but when you do that an inclined plane intersecting a unit (or celestial) sphere sphere doesn't really give you a sinusoidal wave in spherical coordinates.
Lifted from this answer to Analytical expression for the ground track of the International Space Station:
For an inclination $i$ and intersection along the $x$ axis the intersection can be described parametrically as:
\begin{align}
x & = \cos t \\
y & = \sin t \ \cos i\\
z & = \sin t \ \sin i\\
\end{align}
where $t$ is the distance traveled around the circle from 0 to $2 \pi$, which you can think of as one orbit or one year, and
\begin{align}
\varphi & = \arctan2(y, x)\\
\theta & = \arcsin(z).\\
\end{align}
import numpy as np
import matplotlib.pyplot as plt
halfpi, pi, twopi = [f*np.pi for f in (0.5, 1, 2)]
to_degs, to_rads = 180/pi, pi/180
incs = to_rads * np.arange(0, 90, 11)
t = to_rads * np.arange(-179, 180) # left out endpoints to avoid wraparound in plot
ct, st = np.cos(t), np.sin(t)
curves = []
for inc in incs:
cinc, sinc = np.cos(inc), np.sin(inc)
x, y, z = ct, st * cinc, st * sinc
phi = np.arctan2(y, x)
# phi = np.mod(phi + pi, twopi) - pi
theta = np.arcsin(z)
curves.append((inc, theta, phi))
plt.figure()
m, n = 9, 10
for i, (inc, theta, phi) in enumerate(curves):
plt.plot(to_degs * phi, to_degs * theta)
plt.plot(to_degs * phi[m::n], to_degs * theta[m::n], '.k')
plt.xlim(-180, 180)
plt.ylim(-90, 90)
plt.title('inclinations: 0, 11, 22, 33, 44, 55, 66, 77, 88 degrees')
plt.xlabel('RA', fontsize=12)
plt.ylabel('Dec', fontsize=12)
plt.gca().set_aspect('equal')
plt.show()