File:A Hornblower Chronology.svg
Page contents not supported in other languages.
Tools
Actions
General
In other projects
Appearance
Size of this PNG preview of this SVG file: 451 × 600 pixels. Other resolutions: 180 × 240 pixels | 361 × 480 pixels | 577 × 768 pixels | 770 × 1,024 pixels | 1,540 × 2,048 pixels | 940 × 1,250 pixels.
Original file (SVG file, nominally 940 × 1,250 pixels, file size: 63 KB)
This is a file from the Wikimedia Commons. Information from its description page there is shown below. Commons is a freely licensed media file repository. You can help. |
Summary
DescriptionA Hornblower Chronology.svg |
English: A chronology of the Hornblower stories by C. S. Forester, showing timelines of the publication and plots of each, and of Hornblower's rank. Novels are numbered in order of the dates of the described action and connected together to show the sequence of publication. Short stories are labelled with letters, in a smaller font, and are connected with dashed lines. |
Date | |
Source | Own work |
Author | Jaa101 |
Code to Generate the SVG
The following Python3 code generates the SVG and is provided here under the same licence. The easiest way to update the SVG will be to modify the code and rerun Python. After doing so, also update the following code, including the version timestamp.
#
# Used to create the wikimedia version of
# https://commons.wikimedia.org/wiki/File:A_Hornblower_Chronology.svg
# timestamped 15:34, 14 February 2023
# Licensed under the same terms as the generated file on wikimedia.
#
import svgwrite
import datetime
pubYears = [*range(1937, 1967)] # years of publication
discontinuity = 1824
actYears = [*range(1794, discontinuity + 1), 1848] # years of described action
# 17 Hornblower novels and short stories with abbreviated titles
# [ Title, novel?, publication date, action start date, action end date, action skew, publication skew ]
pubs = [
['Midshipman', True, [1950, 3, 13], [1794, 1, 2], [1799, 2, 25], 0, 0],
['Hand of Destiny', False, [1940, 11, 19], [1796, 11, 8], [1796, 11, 15], 8, -3],
['Widow McCool', False, [1950, 12, 5], [1799, 11, 1], [1800, 7, 28], 0, 0],
['Lieutenant', True, [1952, 2, 11], [1800, 8, 25], [1803, 3, 9], 0, 0],
['Hotspur', True, [1962, 7, 27], [1803, 4, 2], [1805, 5, 15], 0, 0],
['Crisis', True, [1966, 7, 26], [1805, 5, 17], [1805, 6, 4], 0, 3],
['Atropos', True, [1953, 9, 10], [1805, 12, 23], [1806, 9, 29], 0, 0],
['Happy Return', True, [1937, 2, 4], [1808, 8, 15], [1809, 1, 31], 0, 0],
['Ship of the Line', True, [1938, 3, 18], [1810, 4, 25], [1810, 11, 19], 0, 0],
['Charitable Offering', False, [1941, 1, 14], [1810, 7, 12], [1810, 7, 13], -13, 5],
['Flying Colours', True, [1938, 10, 31], [1810, 12, 3], [1811, 6, 16], 0, 0],
['Commodore', True, [1945, 3, 12], [1812, 4, 11], [1812, 12, 24], 0, 0],
['His Majesty', False, [1940, 3, 19], [1813, 4, 15], [1813, 4, 16], 0, -1],
['Lord', True, [1946, 6, 11], [1813, 10, 16], [1815, 6, 20], 0, 0],
['Point and the Edge', False, [1964, 9, 28], [1819, 8, 25], [1819, 8, 25], 0, 0],
['West Indies', True, [1958, 8, 28], [1821, 5, 4], [1823, 7, 7], 0, 0],
['Last Encounter', False, [1966, 5, 8], [1848, 9, 23], [1848, 12, 23], 0, -3],
]
# Rank, Date appointed, date promoted/demoted from
ranks = [
[['Midshipman'], [1794, 1, 2], [1796, 10, 25]],
[['Acting', 'Lieutenant'], [1796, 10, 25], [1797, 8, 16]],
[['Lieutenant'], [1797, 8, 16], [1800, 10, 31]],
[['Acting', 'Commander'], [1800, 10, 31], [1802, 10, 1]],
[['Lieutenant'], [1802, 10, 1], [1803, 3, 9]],
[['Commander'], [1803, 3, 9], [1805, 6, 7]],
[['Captain'], [1805, 6, 7], [1820, 5, 4]],
[['Admiral'], [1820, 5, 4], [1850, 4, 1]],
]
# key dimensions
sep = 5 # gap between elements
width = 940
height = 1250
ranksWidth = 90
braceWidth = 25
plotWidth = 60
storyWidth = 150
pubHeight = 50
labelHeight = 50
novelRadius = 11.5 # radius of circles used for novels
shortScale = 0.75 # short stories scaled smaller than novels
baseline = 14 # baseline separation at default font size
xHeight = 4.8 # baseline shift to centre text vertically at default font size
graphWidth = width - ranksWidth - plotWidth - storyWidth - 2 * braceWidth - 7 * sep
graphHeight = height - pubHeight - labelHeight - 4 * sep
pxPerYear = 30
pxPerPubYear = (graphWidth - 2 * novelRadius) / len(pubYears)
pxPerActYear = (graphHeight - 2 * novelRadius) / len(actYears)
daysPerYear = 365.2425
pubOrigin = datetime.date(pubYears[0], 1, 1)
def pubDateX(year, month, day):
return (datetime.date(year, month, day) - pubOrigin).days / daysPerYear * pxPerPubYear
actOrigin = datetime.date(actYears[0], 1, 1)
def actDateY(year, month, day):
t = (datetime.date(year, month, day) - actOrigin).days / daysPerYear
if year > actYears[-2]: # handle action year discontinuity leaving a one-year gap
t += actYears[-2] - actYears[-1] + 1
return t * pxPerActYear
background = '#f9f9f9'
dwg = svgwrite.Drawing('A_Hornblower_Chronology.svg', (width, height), profile='full', debug=True)
dwg.defs.add(dwg.style('''
text {font-family:sans-serif}
line {stroke:black}
circle {stroke:black; fill:white}
'''))
p = dwg.defs.add(dwg.clipPath(id='clip_path_1'))
p.add(dwg.rect((0, 0), (width, height - sep)))
clipped = svgwrite.container.Group(id='clipped', clip_path='url(#clip_path_1)')
dwg.add(clipped)
clipped.add(dwg.rect((0, 0), (width, height), fill=background))
graphTop = pubHeight + labelHeight + 3 * sep + novelRadius
graphLeft = ranksWidth + braceWidth * 2 + plotWidth + storyWidth + sep * 6 + novelRadius
leftBraceG = svgwrite.container.Group(id='leftBrace')
leftBraceG.translate(ranksWidth + braceWidth + sep * 2, graphTop)
leftBraceG.scale((-1, 1))
clipped.add(leftBraceG)
plotG = svgwrite.container.Group(id='plot')
plotG.update({'text-anchor': 'middle'})
plotG.translate(ranksWidth + braceWidth + sep * 3, graphTop)
clipped.add(plotG)
rightBraceG = svgwrite.container.Group(id='rightBrace')
rightBraceG.translate(ranksWidth + braceWidth + plotWidth + sep * 4, graphTop)
clipped.add(rightBraceG)
storyG = svgwrite.container.Group(id='story')
storyG.update({'text-anchor': 'start'})
storyG.translate(ranksWidth + braceWidth * 2 + plotWidth + sep * 5, graphTop)
clipped.add(storyG)
pubG = svgwrite.container.Group(id='pub')
pubG.update({'text-anchor': 'start'})
pubG.translate((graphLeft, pubHeight + sep))
clipped.add(pubG)
labelG = svgwrite.container.Group(id='label')
labelG.update({'text-anchor': 'middle'})
labelG.translate((graphLeft, pubHeight + sep * 2))
clipped.add(labelG)
graphG = svgwrite.container.Group(id='graph')
graphG.update({'text-anchor': 'middle'})
graphG.translate((graphLeft, graphTop))
clipped.add(graphG)
ranksG = svgwrite.container.Group(id='ranks')
ranksG.update({'text-anchor': 'end'})
ranksG.translate(ranksWidth + sep, graphTop)
clipped.add(ranksG)
arrowhead = dwg.polyline([(0, 1.5), (4, 0), (0, -1.5)])
arrow = dwg.marker(insert=(7.4, 0), size=(15, 15), orient='auto')
arrow.viewbox(minx=-5, miny=-5, width=10, height=10)
arrow.add(arrowhead)
dwg.defs.add(arrow)
arrowShort = dwg.marker(insert=(7.8, 0), size=(20, 20), orient='auto')
arrowShort.viewbox(minx=-5, miny=-5, width=10, height=10)
arrowShort.add(arrowhead)
dwg.defs.add(arrowShort)
arrowShort2 = dwg.marker(insert=(9.3, 0), size=(20, 20), orient='auto')
arrowShort2.viewbox(minx=-5, miny=-5, width=10, height=10)
arrowShort2.add(arrowhead)
dwg.defs.add(arrowShort2)
arrowTip = dwg.marker(insert=(0, 0), size=(30, 30), orient='auto')
arrowTip.viewbox(minx=-5, miny=-5, width=10, height=10)
arrowTip.add(arrowhead)
dwg.defs.add(arrowTip)
# Headings
fontScale = 1.25
fontWeight = 'bolder'
def heading(lines, x, y, group, align=None, scale=fontScale):
for line in lines:
label = dwg.text(line, font_weight=fontWeight)
if not align is None:
label.update({'text-anchor': align})
label.translate(x, y)
label.scale(scale)
group.add(label)
y += baseline * scale
heading(['Hornblower', 'Stories'], plotWidth * 0.5, -93, plotG, scale=1.8)
heading(['', 'Ranks'], 0, -30, ranksG)
heading(['Plot', 'Timeline'], plotWidth * 0.5, -30, plotG)
heading(['UK', 'Titles'], 0, -30, storyG)
heading(['(Abbreviated)'], 0, 3, storyG, scale=1)
heading(['Publication', 'Timeline'], -10, -20, pubG, 'end')
yearTick = 20
halfTick = 10
quarterTick = 6
for pub in [*pubYears, pubYears[-1]+1]:
def xyears():
for month in range(1, 13):
x = pubDateX(pub, month, 1)
if month % 12 == 1:
pubG.add(dwg.line((x, -yearTick), (x, 0)))
elif pubYears.count(pub) == 0:
return
elif month % 6 == 1:
pubG.add(dwg.line((x, -halfTick), (x, 0)))
pubYear = dwg.text(str(pub))
pubYear.translate((x + xHeight, -sep - halfTick))
pubYear.rotate(-90)
pubG.add(pubYear)
elif month % 3 == 1:
pubG.add(dwg.line((x, -quarterTick), (x, 0)))
xyears()
for act in [*actYears, actYears[-1] + 1]:
def yyears():
for month in range(1, 13):
y = actDateY(act, month, 1)
if month % 12 == 1:
plotG.add(dwg.line((0, y), (plotWidth, y)))
elif act > actYears[-1]:
return # end after marking the start of the year after the last
elif month % 6 == 1:
plotG.add(dwg.line((0, y), (halfTick, y)))
plotG.add(dwg.line((plotWidth - halfTick, y), (plotWidth, y)))
if act != discontinuity:
actYear = dwg.text(str(act), (0.5 * plotWidth, y + xHeight))
plotG.add(actYear)
elif month % 3 == 1:
plotG.add(dwg.line((0, y), (quarterTick, y)))
plotG.add(dwg.line((plotWidth - quarterTick, y), (plotWidth, y)))
yyears()
pubs.sort(key=lambda pub : datetime.date(*pub[3])) # sort by action date
novelLabel=1
shortLabel='A'
for pub in pubs: # append label letter and (x, y) coordinates to each story
if pub[1]: # Arabic number labels for novels, capital letters for short stories
pub.append(str(novelLabel))
novelLabel += 1
else:
pub.append(shortLabel)
shortLabel = chr(ord(shortLabel) + 1)
pub.append([pubDateX(*pub[2]), (actDateY(*pub[3]) + actDateY(*pub[4])) * 0.5])
def stackText(lines, x, y):
dy = baseline
y -= dy * 0.5 * (len(lines) - 1)
y += xHeight
for line in lines:
ranksG.add(dwg.text(line, (x, y)))
y += dy
zigzagWidth = braceWidth * 2 + plotWidth + storyWidth + graphWidth + sep * 4
zigs = zigzagWidth / 10 # how many zigs give 10-pixel zigs?
zigs = round((zigs + 1) * 0.5) * 2 - 1 # nearest whole odd number of zigs
zig = zigzagWidth / zigs # exact width of zigs needed
ps = f'M 0 {actDateY(1824, 7, 1)} l '
ps += f'{0.5*zig} {-0.5*zig} '
ps += f'{zig} {zig} {zig} {-zig} ' * (zigs // 2)
ps += f'{0.5*zig} {0.5*zig} '
zigzag = dwg.path(ps, stroke_width=4)
zigzag.fill('none')
zigzag.stroke('black')
ranksG.add(zigzag)
zigzag = dwg.path(ps, stroke_width=2)
zigzag.fill('none')
zigzag.stroke(background)
ranksG.add(zigzag)
for rank in ranks:
braceLeft = actDateY(*rank[1])
braceRight = actDateY(*rank[2])
width = braceRight - braceLeft
mid = 0.5 * width
ps = f'M 0 0 '
ps += f'C {0.3*braceWidth} {0.0*mid} {0.5*braceWidth} {0.3*mid} {0.5*braceWidth} {0.5*mid} '
ps += f'C {0.5*braceWidth} {0.8*mid} {0.7*braceWidth} {1.0*mid} {braceWidth} {1.0*mid} '
stackText(rank[0], 0, braceLeft + mid)
ps += f'C {0.7*braceWidth} {width-1.0*mid} {0.5*braceWidth} {width-0.8*mid} {0.5*braceWidth} {width-0.5*mid} '
ps += f'C {0.5*braceWidth} {width-0.3*mid} {0.3*braceWidth} {width-0.0*mid} 0 {width} '
b = dwg.path(ps, stroke_width=2)
b.translate((0, braceLeft))
b.fill('none')
b.stroke('black')
leftBraceG.add(b)
def genBrace(pub, bw):
braceLeft = actDateY(*pub[3])
braceRight = actDateY(*pub[4])
width = braceRight - braceLeft
if pub[5] == 0.5:
ps = f'M 30 0 L {storyWidth} 0'
else:
ps = f'M 30 {pub[5]} '
ps += f'L {0.8*storyWidth} {pub[5]} '
ps += f'C {0.9*storyWidth} {pub[5]} {0.9*storyWidth} 0 {1.0*storyWidth} 0 '
arw = b = dwg.path(ps, stroke_width=0.7)
arw.translate((0, braceLeft + 0.5 * width))
arw.fill('none')
arw.stroke('black')
arw.set_markers((None, None, arrowTip))
storyG.add(arw)
mid = width * 0.5 + pub[5]
ps = f'M 0 0 '
ps += f'C {0.3*bw} {0.0*mid} {0.5*bw} {0.3*mid} {0.5*bw} {0.5*mid} '
ps += f'C {0.5*bw} {0.8*mid} {0.7*bw} {1.0*mid} {bw} {1.0*mid} '
fs = 1 if pub[1] else shortScale
storyG.add(dwg.text(pub[7]+'. '+pub[0], (0, braceLeft + mid + fs * xHeight), font_size=f'{round(fs * 100)}%', stroke_width=7, stroke=background, stroke_linejoin='round'))
storyG.add(dwg.text(pub[7]+'. '+pub[0], (0, braceLeft + mid + fs * xHeight), font_size=f'{round(fs * 100)}%'))
mid = width * 0.5 - pub[5]
ps += f'C {0.7*bw} {width-1.0*mid} {0.5*bw} {width-0.8*mid} {0.5*bw} {width-0.5*mid} '
ps += f'C {0.5*bw} {width-0.3*mid} {0.3*bw} {width-0.0*mid} 0 {width} '
b = dwg.path(ps, stroke_width=2)
b.translate((0, braceLeft))
b.fill('none')
b.stroke('black')
rightBraceG.add(b)
def genPubArrow(pub):
x = pubDateX(*pub[2])
if pub[6] == 0:
ps = f'M 0 0 L 0 {labelHeight}'
else:
ps = f'M 0 0 '
ps += f'C 0 {0.15*labelHeight} {pub[6]} {0.15*labelHeight} {pub[6]} {0.3*labelHeight} '
ps += f'L {pub[6]} {0.7*labelHeight} '
ps += f'C {pub[6]} {0.85*labelHeight} 0 {0.85*labelHeight} 0 {1.0*labelHeight} '
arw = b = dwg.path(ps, stroke_width=0.7)
arw.translate((x, 0))
arw.fill('none')
arw.stroke('black')
arw.set_markers((None, None, arrowTip))
labelG.add(arw)
x += pub[6]
labelG.add(dwg.text(pub[7], (x, 0.5 * labelHeight + xHeight * 0.8), font_size='80%', stroke_width=7, stroke=background, stroke_linejoin='round'))
labelG.add(dwg.text(pub[7], (x, 0.5 * labelHeight + xHeight * 0.8), font_size='80%'))
# arrows between story circles
pubs.sort(key=lambda pub : datetime.date(*pub[2])) # sort by publication date
lastNovel = last = None
for i, pub in enumerate(pubs):
if i > 0:
if pub[1]:
l = dwg.line(tuple(pubs[lastNovel][8]), tuple(pub[8]), stroke_width=2)
l.set_markers((None, None, arrow))
graphG.add(l)
if not (pub[1] and pubs[last][1]):
l = dwg.line(tuple(pubs[last][8]), tuple(pub[8]))
l.dasharray([5, 8])
l.set_markers((None, None, arrowShort2 if pub[1] else arrowShort))
graphG.add(l)
if pub[1]:
lastNovel = i
last = i
# story circles
for pub in pubs:
radius = novelRadius
scale = 1
textScale = 1.25
if not pub[1]:
scale = shortScale
radius *= scale
graphG.add(dwg.circle(tuple(pub[8]), radius))
label = dwg.text(pub[7], font_weight='bolder')
label.translate(pub[8][0], pub[8][1] + xHeight * scale * textScale)
label.scale(scale * textScale)
graphG.add(label)
genBrace(pub, braceWidth)
genPubArrow(pub)
dwg.save()
Licensing
I, the copyright holder of this work, hereby publish it under the following license:
This file is licensed under the Creative Commons Attribution-Share Alike 4.0 International license.
- You are free:
- to share – to copy, distribute and transmit the work
- to remix – to adapt the work
- Under the following conditions:
- attribution – You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.
- share alike – If you remix, transform, or build upon the material, you must distribute your contributions under the same or compatible license as the original.
Items portrayed in this file
depicts
7 February 2023
image/svg+xml
fc5b82029a9efb352847125b27032aa43130592d
64,567 byte
1,250 pixel
940 pixel
File history
Click on a date/time to view the file as it appeared at that time.
Date/Time | Thumbnail | Dimensions | User | Comment | |
---|---|---|---|---|---|
current | 05:34, 14 February 2023 | 940 × 1,250 (63 KB) | Jaa101 | Narrowed. Lighter background. | |
04:59, 14 February 2023 | 1,000 × 1,200 (63 KB) | Jaa101 | Specify size. Correct arrowheads. | ||
03:59, 14 February 2023 | 512 × 512 (63 KB) | Jaa101 | librsvg bug work-arounds. Improvements | ||
08:53, 7 February 2023 | 512 × 512 (56 KB) | Jaa101 | Uploaded own work with UploadWizard |
File usage
The following page uses this file:
Metadata
This file contains additional information, probably added from the digital camera or scanner used to create or digitize it.
If the file has been modified from its original state, some details may not fully reflect the modified file.
Width | 940 |
---|---|
Height | 1250 |
Retrieved from "https://en.wikipedia.org/wiki/File:A_Hornblower_Chronology.svg"