I wanted to setup my own Pomodoro app. This was not too hard; I am using Nicegui for such stuff. The thing that was hard for me was a visual representation; particularly since Nicegui does not provide something usable out of the box.
After searching around a bit, I first found their example creating a visual clock and updating it every second. With that in mind, I decided to understand the SVG they were drawing and to update it according to my needs. And I can say, it worked out! My result looks something like this:
| Countdown running | Empty clock |
|---|---|
For the left image, the concept is that about three quarters of the time are still remaining.
I would like to write down what I learned on the way.
The SVG elements
The image I drew is based on this file from Wikipedia, available via the Creative Common License.
I will discuss multiple aspects of the file here.
Image Size
I kept most of it as it was there, like the size: My image is $800 \times 800$ pixels. This is important to keep fixed, as the defined coordinates build on this one.
I had to do a bit of a redefinition from their setup: They used the width and height part, but I changed that and used the viewBox instead. That way, I could scale in my application as required but keep their coordinates.
Background: A white circle
This one was easy. Start with a white circle that will be the background of the clock. In case you want to see this background here in my blog, use the dark mode - as my background is white in the light theme…
In SVG, a circle is really easy:
| |
We define the coordinates of the center (cx and cy), we define a radius r and we define a color (fill). As easy as that.
Markers
These were the most elaborate part to be drawn. Thankfully, I did not have to learn that myself; the template provided these for me. But I understood what happened and I want to write that down.
The code is a set of nested <g> elements. Note the ids of each element, as I will reference these.
| |
I will discuss each element. It makes more sense to start from the inside:
- Start with
id="e". We have got two things here:- A path with
id="b". This path exactly describes the marker for 12:01. - Then, there is a
<use transform="...">element. This one rotates the 12:01 bar for $6$ degrees, giving us the 12:02 bar. - The resulting image contains not too much:
- A path with
- Below the
id="e", we have another transform that rotatesbfor $12$ degrees. This one gives us the bars for 12:03 and 12:04. - Now consider
id="a". This one contains multiple things:- A
path. This one represents the 12:05 bar, which is larger than the other bars created until here. - The
id="e"element, containing all small bars from 12:01 up to 12:04. - We now get four small and one big marker:
- A
- Next out is
id="d". In this one we have:- A new bar that will represent the 12:00 bar.
- The bars from 12:01 up to 12:05, as
ais a child ofd. - Two transformation of
a, giving us he 12:06-12:10 and 12:11-12:15 range. - So, in total,
dgives us all bars from 12:00 up to 12:15. - Our clock starts to get a shape:
- Now there are two ways how to continue here:
- Rotate
donce to get the right half of the clock (this happens inc) and then mirror this new thing to the other side (this is done in the last line).
I kept with the original and chose this approach. Note that with that, every quarter-hour marker is drawn twice! We don’t really care about that, but I found it important for understanding what’s going on. - Rotate
dfour times to get the same for all four quarters of the clock.
I would have preferred this way, as here I can skip the 12:00 bar indand don’t have to draw them duplicated.
Still, I decided to keep the original at the moment. - With this, we are done!
- Rotate
Red countdown filling
I learned two interesting things here:
- I had to learn about SVG’s
<path>element, and the possibility to draw arcs here - I had to do a bit of maths for finding out the end points of the arcs.
In general, the red filling in my drawing does the following:
- Start on top
- Draw an arc for the given fraction of the circle
- Draw a straight line to the center
- Go back to the beginning
In general, I did this with a path in SVG which I defined to be filled. For the image shown on top, my path looks like this1:
| |
Most of this looks easy! In general, we only have three things to consider:
- Use a
<path>element. - Define where the path goes via
d. - Add the red color via
fill.
The arc
Now the obvious complicated part is the d attribute. I want to discuss that in a bit more detail.
Thankfull, multi-line attributes work, so I was able to split this up a bit. Let’s consider each line:
M400,100: This defines where the path should start: At coordinates400,100. This is the top corner of the red element.
Without this, the path starts at the top left point at0, 0, which I definitely don’t want here.A...: This defines the rounded part of the path.
The general setup of this element is:A rx,ry rotation,large-arc-flag,sweep-flag x,y. Commas are optional and can be replaced with spaces, but this is the version I find most readable. Let’s discuss what this means:- Technically, we are not drawing a circle here, but an ellipse. This is important to remember!
rx,ry: For an ellipse, these are the lengths of the two axes. For a circle, both should be the same number.rotation: In the default case, the major semi-axis of the ellipsis is horizontal; this argument defines how much to rotate it. For a circle, you may use any number here; it does not matter and returns the same thing always. So let’s just keep it at zero.large-arc-flag: If the ellipsis is defined and we are given two points on the border of the ellipsis, there are two ways to get from one point to the other one; one of them typically is shorter than the other one. Set this flag to1if your path shold take the longer way, and0otherwise.sweep-flag: We will have defined all dimensions of the ellipse/circle. Now given two points, there are two ellipses that have these dimensions and that have both points on their boundary. Thesweep-flagessentially defines which of the two ellipse to choose.
I had to play around with these arguments a bit to understand what they mean, so please do the same if you want a better intuition.x,y: These are the end points where the arc should stop.
- Now my arc definition looks like this:
A300,300 0,0,0 101,373.
This means that we will look for a circle with radius300pxwith the following two points on their boundary:400,100(when starting the arc, the path is currently in this position) and101,373(the end point is defined in the arc configuration). Thesweep-flagandlarge-arg-flagmight change, but are correct for this case. - Finally, from the end point, go to the center of the clock.
L400,400does exactly that: Draw a straight line from the previous point (which was101,373) to400,400. - Now for the filling, the curve is closed by adding a straight line from the last point to the starting point. And we are done!
End point of the arc
Obviously, I did not choose 101,373 accidentially. Finding these coordinates was more of a mathematical challenge. The idea here is simple: Start at the top point of the circle, and rotate the point by a given number of degrees.
For computing them, I created a function that receives the ratio of the circle that I want to fill as an input. Let’s start with a bit of theory:
First, find out which point we want to rotate. This is actually simple: The part of the circle is starting at the top, so at 400,100. Note that the coordinate system of SVG is a bit counterintuitive - the y axis is kind of reversed. This will play a role again later on.
This was a merely theoretical concept. Here’s the next one: How do we rotate?
In maths, a rotation is done with a matrix. Given a point $(x_0, y_0)$, we get a rotation for angle $\alpha$ in clockwise direction by doing a matrix multiplication. $$ \left[\begin{matrix} x_1 \newline y_1 \end{matrix}\right] = \left[\begin{matrix} \cos(\alpha) & -\sin(\alpha) \\ \sin(\alpha) & \cos(\alpha) \end{matrix}\right] \cdot \left[\begin{matrix} x_0 \newline y_0 \end{matrix}\right] $$
This rotation happens around the origin, so we need to transform our circle to that. Thankfully we know that our circle center is $(400, 400)$, so it is sufficient to compute $x_{0, new} = x_0 - 400$ and $y_{0, new} = y_0 - 400$.
As the final theoretical concept, we need to find the angle $\alpha$, based on the ratio we want to fill. In general, this is easy: Since $2 \pi$ is a full circle, we need $ratio \cdot 2 \pi$ as our angle. But here comes SVG in again with their weird coordinate system: Due to that, we need the angle $(1 - ratio) \cdot 2 \pi$…
So, summing this all up, I just created a Python function that does all this stuff:
| |
Conclusion
Drawing stuff on the computer is quite a bit of math. Also, SVG is a fun thing to work with!
Note that I have rounded the coordinates here. These are not totally correct, but they make the code more readable. ↩︎