Skip to content

Instantly share code, notes, and snippets.

@ngregoire
Last active April 6, 2024 04:33
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ngregoire/cbddac55bf8ba25965302e3bbf75dc31 to your computer and use it in GitHub Desktop.
Save ngregoire/cbddac55bf8ba25965302e3bbf75dc31 to your computer and use it in GitHub Desktop.
Matplot script used to generate timelines
The script requires Python3 and the packages `numpy`, `pandas` and `matplotlib`.
It accepts a input file compatible with Mermaid (cf `bb.data`) and generates a PNG file.
The tag `<br/>` is supported, so that a label can be displayed on several lines.
I use the font `Humor Sans`, that can be installed via `apt install fonts-humor-sans`.
timeline
title Bug Bounty
2002 : iDefense
2004 : Firefox
2005 : ZDI
2007-03-01 : My first<br>ZDI report
2010 : Google
2011 : Facebook
2012 : HackerOne
2012-09-10 : Bugcrowd
2013-10-01 : Yahoo's<br> "t-shirt gate"
2014-01-01 : My first<br>Web bounty
2015 : YesWeHack
2016 : Intigriti
2016-08-08 : H1-702 in Vegas
2016-12 : Uber's "ransom"
2019-03 : Santiago Lopez<br>reaches $1M<br>in bounties (on h1)
2020-12 : Immunefi
#!/usr/bin/env python3
import sys
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
#############
# CONSTANTS #
#############
STYLES = {
"timeline": "solid",
"marker": "o",
"label_alignment": "center",
"x_alignment": "center",
"x_rotation": 30,
}
COLORS = {
"timeline": "black",
"marker": "black",
"stem": "black",
"label": "olive",
"x_label": "salmon",
"x_ticks": "black",
}
ENDS = {
"left": {"event": "25 years ago", "date": "15/03/1999"},
"right": {"event": "Now", "date": "15/03/2024"},
}
#############
# FUNCTIONS #
#############
def read_data(fname):
events = []
dates = []
try:
# Read the file
lines = open(fname, mode="r").readlines()
# Extract title
title = lines[1].split("title ")[1].strip()
# Process data points
for data_point in lines[2:]:
# Remove comments
data_point = data_point.split("#", 1)[0].strip()
# Skip empty lines
if data_point=="":
continue
# Split by the first colon
when, what = data_point.split(":", 1)
# Add to arrays
events.append(what.strip())
dates.append(pd.Timestamp(when.strip()))
# Add fixed data points
events = [ENDS["left"]["event"]] + events + [ENDS["right"]["event"]]
dates = [pd.Timestamp(ENDS["left"]["date"])] + dates + [pd.Timestamp(ENDS["right"]["date"])]
# Catch exceptions
except ValueError as e:
print(f"[!] Can't process data point '{data_point.strip()}' from file '{fname}'\nError:", e)
exit(-1)
except FileNotFoundError as e:
print(f"[!] Can't read file '{fname}'\nError:", e)
exit(-1)
except PermissionError as e:
print(f"[!] Can't access file '{fname}'\nError:", e)
exit(-1)
# Return the collected data
return title, pd.DataFrame({ 'event': events, 'date': dates }).sort_values(by="date")
def validate_indexes(dataframe):
indexes_ok = True
expected_i = 0
for i in dataframe.index:
if i != expected_i:
print(f"Index {i} should be {expected_i}")
indexes_ok = False
expected_i = expected_i + 1
if indexes_ok:
print("Indexes are OK")
else:
print("Indexes aren't correctly sorted")
#############
# MAIN CODE #
#############
# Read the text file describing the timeline
src_file = sys.argv[1]
fig_name, df = read_data(src_file)
print(f"==== DataFrame\n{df=}")
# Check if the data is correctly sorted
print("==== Validating indexes")
validate_indexes (df)
# Define levels for labels
levels = np.tile(
[ -3, 3, -2, 2, -1, 1],
int(np.ceil(len(df)/6))
)[:len(df)]
# Use the XKCD theme
print("==== Generating plot")
with plt.xkcd(length=100.0, randomness=3.0):
# Create the Matplot figure
fig, ax = plt.subplots(figsize=(12.8,7.2), constrained_layout=True)
# Define margins
ax.margins(y=0.1)
# Draw the timeline itself
ax.plot(
df['date'],
np.zeros_like(df['date']),
marker=STYLES["marker"],
linestyle=STYLES["timeline"],
color=COLORS["timeline"],
markerfacecolor=COLORS["marker"],
)
# Draw the vertical stems
ax.vlines(df['date'], 0, levels, color=COLORS["stem"])
# Add labels
for date, level, event in zip(df['date'], levels, df['event']):
ax.annotate(
event.replace("<br>", "\n"),
color=COLORS["x_label"] if event in [ENDS["left"]["event"], ENDS["right"]["event"]] else COLORS["label"],
xy=(date, level),
xytext=(-3, np.sign(level)*3),
textcoords="offset points",
ha=STYLES["label_alignment"],
va="bottom" if level > 0 else "top"
)
# Format the x-axis
## Major
ax.xaxis.set_major_locator(mdates.YearLocator(5))
ax.xaxis.set_major_formatter(mdates.DateFormatter("%b %Y"))
ax.tick_params(which='major', length=10, width=2, color=COLORS["x_ticks"])
## Minor
ax.xaxis.set_minor_locator(mdates.YearLocator(1))
ax.tick_params(which='minor', length=5, width=2, color=COLORS["x_ticks"])
# Add labels to the x-axis
plt.setp(ax.get_xticklabels(), color=COLORS["x_label"], rotation=STYLES["x_rotation"], ha=STYLES["x_alignment"])
# Remove y-axis and spines
ax.yaxis.set_visible(False)
ax.spines["left"].set_visible(False)
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
# Save to disk
dst_file = src_file.replace(".data", ".png")
plt.savefig(dst_file)
print("==== Done")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment