Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate multiple svg graphs #532

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
pygal==3.0.0
beautifulsoup4>=4.12.2,<=5.0
lxml>=5.0.0,<=6.0
matplotlib>=3.8.2,<=4.0
pandas>=2.1.4,<=3.0
186 changes: 113 additions & 73 deletions src/imgs/src/graph_render.py
Original file line number Diff line number Diff line change
@@ -1,82 +1,122 @@
import pygal
from pygal.style import Style
import math
from sys import argv, stderr

# ------------------------------------------------------------------------------------------------
# Configuration Constants
# ------------------------------------------------------------------------------------------------
import lxml
import matplotlib.pyplot as plt
import pandas as pd
from bs4 import BeautifulSoup
from dataclasses import dataclass
from os import remove

# Rotation of X-Axis labels in degrees
x_label_rotation = 0.01
@dataclass
class GraphData:
dataset_keys: [str]
file_path: str
title: str

# Number of labels to be displayed on the X-Axis
x_label_count = 10

# ------------------------------------------------------------------------------------------------
# The first line of the file must contain the X- and Y-Axis labels seperated by commas.
# The following lines are expected to contain the graph data in a comma-separated format
# and in the same order as the Axis labels.
# ------------------------------------------------------------------------------------------------
# @[GraphData] graph_data_list: an array of GraphData structs
def gen_graphs(graph_data_list: [GraphData]):
for data in graph_data_list:
gen_graph(data.dataset_keys, data.file_path, data.title)

def gen_graph(in_path, title):
custom_style = Style(
font_family="Inter",
label_font_size=12,
major_label_font_size=12,
title_font_size=16

# @[string] dataset_keys:
# @see https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.set_index.html#pandas.DataFrame.set_index
# for more info on dataset keys.
# @string in_path: path to CSV file to generate data from.
# @string title: name for the graph.
def gen_graph(dataset_keys, in_path, title):
## Let's draw the plot

plt.rcParams["figure.figsize"] = [7.50, 3.50]
plt.rcParams["figure.autolayout"] = True
plt.rcParams["font.family"] = "Inter"
# Assume fonts are installed on the machine where the SVG will be viewed
# (we load Inter with the webpage so it should be there)
plt.rcParams["svg.fonttype"] = "none"

# Those are just used to "fingerprint" the resulting elements in the SVG export,
# they will be replaced by CSS variables
COLOR_BASE = "#FFCD01".lower()
COLOR_LINE = "#FFCD02".lower()

# Set everything to the base color
plt.rcParams["text.color"] = COLOR_BASE
plt.rcParams["axes.labelcolor"] = COLOR_BASE
plt.rcParams["xtick.color"] = COLOR_BASE
plt.rcParams["ytick.color"] = COLOR_BASE

# Read the values to plot from the input CSV
df = pd.read_csv(in_path)

# Set the color of the actual plot line to the secondary color
plot = df.set_index(dataset_keys).plot(
legend=None, gid="fitted_curve", color=COLOR_LINE
)

# Create Line Chart Object and Open File
chart = pygal.Line(
height=450,
show_dots=False,
show_legend=False,
show_minor_x_labels=False,
x_label_rotation=x_label_rotation,
style=custom_style
# Add grid lines on the y values
plot.yaxis.grid(True)

# Set the color of the plot box to the base color too
plt.setp(plot.spines.values(), color=COLOR_BASE)

# Add title at the top
title_filename = title.replace(' ', '_')
plt.title(title)
plt.ylabel(df.columns[1])
plt.savefig(f"{title_filename}_temp.svg", transparent=True)

## Manipulate the SVG render of the plot to replace colors with CSS variables
## Writes to a temp SVG file to prepare writing the SVG "soup"
with open(f"{title_filename}_temp.svg", "r") as f:
contents = f.read()
# It's an SVG, so let's use the XML parser
soup = BeautifulSoup(contents, "xml")

replace_style_property(soup, "path", "stroke", COLOR_BASE, "var(--fg)")
replace_style_property(soup, "path", "stroke", COLOR_LINE, "var(--inline-code-color)")
replace_style_property(soup, "text", "fill", COLOR_BASE, "var(--fg)")

# Write the altered SVG file
with open(f"{title_filename}.svg", "wb") as f_output:
f_output.write(soup.prettify("utf-8"))
print(soup)

# Remove temp SVG file
remove(f"{title_filename}_temp.svg")


def replace_style_property(
soup, element_name, css_property, value_to_replace, new_value
):
"""
Given a `Soup`, a CSS `property` applied inline, replace the a `specific value`
this property can assume with `another` one in all the elements with the specified `name`

E.g. the style of all the "path" elements whith a CSS property "fill" of
"#ffcd01" will change to "var(--fg):

`fill: #ffcd01; font: 12px 'Inter'; text-anchor: middle`
to
`fill: var(--fg); font: 12px 'Inter'; text-anchor: middle`

Soup and soup ResultSet are modified in-place
"""
found_elements = soup.find_all(
element_name,
style=lambda value: value and f"{css_property}: {value_to_replace}" in value,
)
csv = open(in_path, "r").readlines()

# Set Chart and Axis Titles
chart.title = title
headers = csv.pop(0).split(",")
chart.x_title = headers[0]
chart.y_title = headers[1]

# Generate label spacing variables
min_x_val = float(csv[0].split(",")[0])
max_x_val = float(csv[len(csv) - 1].split(",")[0])
x_mod_val = (max_x_val - min_x_val) / x_label_count

# Generate graph data arrays
x_labels = []
x_labels_major = []
y_data = []
last_x = None
for line in csv:
# Add data to label arrays
data = line.split(",")
x_labels.append(data[0])
y_data.append(float(data[1]))

# Check if current X-Label should be Major Label
xval_float = float(data[0])
if last_x is not None and ((last_x % x_mod_val) > (xval_float % x_mod_val)):
x_labels_major.append(math.floor(xval_float))
x_labels.append(math.floor(xval_float))
last_x = xval_float

# Load graph data into chart object and save to file
chart.x_labels = x_labels
chart.x_labels_major = x_labels_major

chart.add("", y_data)
print(chart.render(is_unicode=True))


if len(argv) != 3:
print("Usage: python3 graph_render.py <path/to.csv> <graph title>", file=stderr)
exit(1)

gen_graph(argv[1], argv[2])
# Replace the color magic value with the CSS variable
for element in found_elements:
element["style"] = element["style"].replace(
f"{css_property}: {value_to_replace}", f"{css_property}: {new_value}"
)

return

# uncomment if wish to be used from the command line
# if len(argv) != 3:
# print("Usage: python3 graph_render.py <path/to.csv> <graph title>", file=stderr)
# exit(1)

# gen_graph(argv[1], argv[2])