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 runningEmpty 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:

<circle cx="400" cy="400" r="400" fill="#fff"></circle>

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.

<!-- #c: All bars from 12:00 up to 12:30 -->
<g id="c">
    <!-- #d: All bars from 12:00 up to 12:15 -->
    <g id="d">
        <!-- 12:00 bar -->
        <path d="m400 40v107" stroke="#000" stroke-width="26.7" />
        <!-- #a: All bars from 12:01 up to 12:05 -->
        <g id="a">
            <!-- 12:05 bar -->
            <path d="m580 88.233-42.5 73.612" stroke="#000" stroke-width="26.7"/>
            <!-- #e: Two small bars for 12:01 and 12:02 -->
            <g id="e">
                <!-- 12:01 bar -->
                <path id="b" d="m437.63 41.974-3.6585 34.808" stroke="#000" stroke-width="13.6" />
                <!-- Copy the 12:01 bar to 12:02 -->
                <use transform="rotate(6 400 400)" xlink:href="#b" />
            </g>
            <!-- Copy the 12:01 and 12:02 bars to 12:03 and 12:04 -->
            <use transform="rotate(12 400 400)" xlink:href="#e" />
        </g>
        <!-- Add 12:06 up to 12:10 bars -->
        <use transform="rotate(30 400 400)" xlink:href="#a" />
        <!-- Add 12:11 up to 12:15 bars -->
        <use transform="rotate(60 400 400)" xlink:href="#a" />
    </g>
    <use transform="rotate(90 400 400)" xlink:href="#d" />
</g>
<use transform="matrix(-1,0,0,1,800,0)" xlink:href="#c"/>

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:
  • Below the id="e", we have another transform that rotates b for $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:
  • 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 a is a child of d.
    • Two transformation of a, giving us he 12:06-12:10 and 12:11-12:15 range.
    • So, in total, d gives 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 d once to get the right half of the clock (this happens in c) 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 d four 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 in d and don’t have to draw them duplicated.
      Still, I decided to keep the original at the moment.
    • With this, we are done!

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:

<path d="M400,100
            A300,300 0,0,0 101,373
            L400,400
        " fill="#f00"></path>

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 coordinates 400,100. This is the top corner of the red element.
    Without this, the path starts at the top left point at 0, 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 to 1 if your path shold take the longer way, and 0 otherwise.
    • 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. The sweep-flag essentially 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 radius 300px with the following two points on their boundary: 400,100 (when starting the arc, the path is currently in this position) and 101,373 (the end point is defined in the arc configuration). The sweep-flag and large-arg-flag might change, but are correct for this case.
  • Finally, from the end point, go to the center of the clock. L400,400 does exactly that: Draw a straight line from the previous point (which was 101,373) to 400,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:

def rotate_point(ratio, x_0=400, y_0=100):

    # Compute the ratio
    alpha = 2 * math.pi * (1 - ratio)

    # Transfer the circle center into the origin
    x_prime = x_0 - 400
    y_prime = y_0 - 400

    # Do the matrix multiplication
    x_new = x_prime * math.cos(alpha) - y_prime * math.sin(alpha)
    y_new = x_prime * math.sin(alpha) + y_prime * math.cos(alpha)

    # Don't forget to transfer the circle back to
    # the original range!
    x_final = x_new + 400
    y_final = y_new + 400

    return x_final, y_final

Conclusion

Drawing stuff on the computer is quite a bit of math. Also, SVG is a fun thing to work with!


  1. Note that I have rounded the coordinates here. These are not totally correct, but they make the code more readable. ↩︎