Generating an SVG clock face


This is a simple Python function to generate a clock face indicating a specified time. It can be used in the script below to generate 1, 2, 4 or 6 faces indicating random times to teach children how to tell the time. Run this script from the command line as

python N -d D

where N is the number of clocks to draw and D is a "difficulty": e (easy: HH:00 times only), m (medium: HH:00 and HH:30), h (hard: HH:00, HH:15, HH:30 and HH:45), v (very hard: any time HH:MM). The additional options -L and -T suppress the minute labels and ticks respectively.

enter image description here

import sys
import random
import argparse
import math

# Difficulty flags

def preamble(fo):
    """The SVG preamble and styles."""

    print('<?xml version="1.0" encoding="utf-8"?>\n'

    '<svg xmlns=""\n' + ' '*5 +
       'xmlns:xlink="" width="{}" height="{}" >'
            .format(width, height), file=fo)

        <style type="text/css"><![CDATA[""", file=fo)

    print('circle {fill:none; stroke-width: 2px; stroke: #000;}', file=fo)
    print('circle.centre-circ {fill:#000;}', file=fo)
    print('line {stroke-width: 2px; stroke: #000;}', file=fo)
    print('text {dominant-baseline: middle; text-anchor:middle;'
          '      font-family:Arial,Helvetica;font-size: 20pt;'
          '      font-weight: bold;}', file=fo)
    print('text.min-labels {font-size: 14pt; font-weight: normal;}', file=fo)
    print(' {stroke-width: 4px; stroke: #f00;}', file=fo)
    print(' {stroke-width: 12px; stroke: #000;}', file=fo)

    </defs>""", file=fo)

def make_clock_face(fo, cx, cy, r):
    """Make the clock face, with numbers and ticks."""

    print('<circle cx="{}" cy="{}" r="{}"/>'.format(cx, cy, r), file=fo)

    def add_tick(x, y, length):
        """Add a tickmark of specifed length at position (x, y)."""

        x1, y1 = (r-length)*x + cx, (r-length)*y + cy
        x2, y2 = r*x + cx, r*y + cy
        print('<line x1="{}" y1="{}" x2="{}" y2="{}"/>'.format(x1,y1,x2,y2),
    hr = -1
    for mn in range(60):
        th = math.pi/30 * mn - math.pi/3
        x, y = math.cos(th), math.sin(th)
        if mn // 5 > hr:
            # This tick is an hour tick so it's a bit longer
            hr += 1 
            xt, yt = (r-40)*x + cx, (r-40)*y + cy
            print('<text x="{}" y="{}">{}</text>'.format(xt,yt,str(hr+1)),
            if min_ticks and min_ticklabels:
                xt, yt = (r+20)*x + cx, (r+20)*y + cy
                print('<text x="{}" y="{}" class="min-labels">{}</text>'
                      .format(xt,yt,str((hr+1)*5 % 60)), file=fo)
            add_tick(x, y, 20)
        if min_ticks:
            # A regular minute tick
            add_tick(x, y, 10)
    print('<circle cx="{}" cy="{}" r="10" class="centre-circ"/>'
                                            .format(cx, cy), file=fo)

def add_clock_hands(fo, cx, cy, r, time):
    """Add the clock hands indicating the provided time."""

    hr, mn = [int(f) for f in time.split(':')]
    assert 0 < hr <= 12
    assert 0 <= mn < 60

    def hand_line(x2, y2, cls):
        print('<line x1="{}" y1="{}" x2="{}" y2="{}" class="{}"/>'.format(
            cx, cy, x2, y2, cls), file=fo)

    # minutes
    th = math.pi/30 * mn - math.pi/2
    x, y = math.cos(th), math.sin(th)
    x2, y2 = r*0.7*x + cx, r*0.7*y + cy
    hand_line(x2, y2, 'mn-hand')

    # hours
    th = math.pi/6 * hr - math.pi/2 + mn / 60 * math.pi / 6
    x, y = math.cos(th), math.sin(th)
    x2, y2 = r*0.5*x + cx, r*0.5*y + cy
    hand_line(x2, y2, 'hr-hand')

def add_clock(cx, cy, r, time):
    """Add a clock indicating the given time centred at cx,cy."""

    add_clock_hands(fo, cx, cy, r, time)
    make_clock_face(fo, cx, cy, r)

def get_random_times(n, difficulty):
    """Return a list of random times of some specified difficulty."""

    times = []
    for i in range(n):
        hr = random.randint(1,12)
        if difficulty == MEDIUM:
            mn = random.randint(0,1)*30
        elif difficulty == HARD:
            mn = random.randint(0,3)*15
        elif difficulty == VERYHARD:
            mn = random.randint(0,59)
            mn = 0
    return times

parser = argparse.ArgumentParser(description='Create clock faces to help'
    ' learning the time.')
parser.add_argument('n', help='The number of clocks to draw',
    default=1, type=int, choices=(1, 2, 4, 6))
parser.add_argument('-d', '--difficulty', dest='difficulty', nargs='?',
    default=MEDIUM, choices=DIFFICULTIES)
parser.add_argument('-T', '--no-minute-ticks', dest='no_min_ticks',
    help='Suppress minute tick marks around the inside of the clock',
    default=False, action='store_true')
parser.add_argument('-L', '--no-minute-ticklabels', dest='no_min_ticklabels',
    help='Suppress minute tick labels around the outside of the clock',
    default=False, action='store_true')
args = parser.parse_args()
min_ticks = not args.no_min_ticks
min_ticklabels = not args.no_min_ticklabels
n = args.n
assert n in (1, 2, 4, 6)
ncols = 1
if n > 2:
    ncols = 2
nrows = n // ncols
difficulty = args.difficulty
times = get_random_times(n, difficulty)

# We've got the parameters: los geht's!
width = 800
height = 800 * nrows // ncols
cwidth = cheight = width // ncols
r = cwidth * 0.4
with open('time.svg', 'w') as fo:
    for i, time in enumerate(times):
        print('{:2d}:{:02d}'.format(*[int(s) for s in time.split(':')]))
        cy = (i // ncols) * cwidth + cwidth // 2
        cx = (i % ncols) * cheight + cheight // 2
        add_clock(cx, cy, r, time)
    print('</svg>', file=fo)

Another example: python 6 -d v:

enter image description here

Current rating: 3.8


Comments are pre-moderated. Please be patient and your comment will appear soon.

Ed 4 years, 5 months ago

Thank you so much! I was just about to create something like this myself to help teach my son to read. But now I can just use your code instead!

Link | Reply
Currently unrated

archery guru 4 years ago

Thanks so much for sharing this. I was planning on writing a similar script to teach my daughter how to tell time.

Link | Reply
Currently unrated

Anonymous 1 year, 10 months ago

Thanks for sharing!
If you do not have numpy installed, just replace "np" with "math" and import "math" instead of "numpy as np" - the script only uses trig functions.

Link | Reply
Currently unrated

christian 1 year, 10 months ago

Thank you for your comment. This is true: I've updated the code to remove the numpy dependency so now it runs on just the standard library routines.

Link | Reply
Currently unrated

New Comment


required (not published)