A website for producing interactive charts without writing a single line of code. Built with Common Lisp and Python.
https://charts.craigoates.net
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
251 lines
10 KiB
251 lines
10 KiB
#!/usr/bin/env python |
|
from bokeh.plotting import figure, output_file, save, show |
|
from bokeh.models import Legend, FactorRange |
|
from bokeh.palettes import Spectral4 |
|
import pandas as pd |
|
import numpy as np |
|
import os.path |
|
from sys import stdin |
|
import argparse |
|
import logging |
|
from pathlib import Path |
|
from rich import print |
|
from rich.console import Console |
|
from rich.markdown import Markdown |
|
from rich.logging import RichHandler |
|
from rich.traceback import install |
|
import random |
|
|
|
# Global Variables (Making things easy for myself) |
|
# ==================================================================== |
|
__version__ = "1.0.0" |
|
logging.basicConfig(level="NOTSET", format="%(message)s", |
|
datefmt="[%X] ", handlers=[RichHandler()]) |
|
log = logging.getLogger("rich") |
|
console = Console() |
|
# ==================================================================== |
|
|
|
def get_extension(path): |
|
return os.path.splitext(path)[1] |
|
|
|
def parse_arguments(): |
|
parser = argparse.ArgumentParser("hot-line-python") |
|
parser.add_argument("-v", "--version", action="version", |
|
version='%(prog)s: {version}'.format(version=__version__)) |
|
parser.add_argument("-V", "--verbose", |
|
help="Provide info. on the current state of the chart creation.", |
|
action="store_true") |
|
parser.add_argument("input", |
|
help="The path of the data file which hot-line will use to make the chart..") |
|
parser.add_argument("output", |
|
help="The location hot-line will write the newly created chart to.") |
|
parser.add_argument("-t", "--title", |
|
help="The title displayed on the chart.") |
|
parser.add_argument("-x", "--xaxis", |
|
help="The label used on the chart's x-axis.") |
|
parser.add_argument("-y", "--yaxis", |
|
help="The label used on the chart's y-axis.") |
|
|
|
return parser.parse_args() |
|
|
|
def get_y_values(data): |
|
r = (data.loc[0]) |
|
return r[1::1] |
|
|
|
def random_colour(): |
|
r = random.randint(0,255) |
|
g = random.randint(0,255) |
|
b = random.randint(0,255) |
|
rgb = [r,g,b] |
|
return rgb |
|
|
|
# I could reduce the duplicated code by extracting the 'verbose' code out but I |
|
# don't plan on touching this much after it's able to generate charts. So, I'm |
|
# at a point where I don't care. If this script needs to expand, then I will |
|
# worry about refactoring and making the code pretty then. |
|
def build_chart(arguments): |
|
column_data = [] |
|
column_headers = [] |
|
x_values = [] |
|
legend_items = [] |
|
if arguments.verbose is True: |
|
tasks = ["Loading file: Complete", |
|
"Parsing data: Complete", |
|
"Building chart outline: Complete", |
|
"Populating chart: Complete", |
|
"All Done"] |
|
tasks_total = len(tasks) |
|
with console.status("[bold green]Performing magic...") as status: |
|
try: |
|
# 1. Load file... |
|
extension = get_extension(arguments.input) |
|
if extension == ".tsv": |
|
data = pd.read_csv(arguments.input, sep="\t", header=None, |
|
index_col=False, dtype='unicode') |
|
elif extension == ".csv": |
|
data = pd.read_csv(arguments.input, sep=",", header=None, |
|
index_col=False, dtype='unicode') |
|
else: |
|
log.critical("File is neither a .csv or .tsv file. Unable to process.") |
|
return |
|
y_labels = get_y_values(data) |
|
data = data[1:] |
|
task = tasks.pop(0) |
|
console.log(f"1/{tasks_total}. {task}") |
|
|
|
# 2. Parse data... |
|
for x in data.columns: |
|
values = data.iloc[:, x] |
|
column_data.append(values) |
|
x_values = column_data[0] |
|
column_data.pop(0) |
|
task = tasks.pop(0) |
|
console.log(f"2/{tasks_total}. {task}") |
|
|
|
# If x_values contains duplicated values, they will |
|
# need an 'ID' appended to each value. This part of |
|
# the code checks for them so the code below will know |
|
# to append it or not. If there are no duplicated |
|
# entries, the appendaged is not needed. |
|
dupes = False |
|
for item in x_values.duplicated(): |
|
if item is False: |
|
dupes = True |
|
break |
|
|
|
# Add an 'ID' tag to the x-axis values. This is done |
|
# to stop duplicated values which causes errors in |
|
# Bokeh. |
|
x_range = [] |
|
if dupes == True: |
|
counter = 0 |
|
for row_value in x_values: |
|
x_range.append(f"{counter}: {row_value}") |
|
counter = counter + 1 |
|
else: |
|
x_range = x_values |
|
|
|
# 3. Build chart outline... |
|
p = figure(title=arguments.title, |
|
x_range=FactorRange(* x_range), |
|
x_axis_label=arguments.xaxis, |
|
y_axis_label=arguments.yaxis, |
|
sizing_mode="stretch_both") |
|
p.axis.major_label_text_font_size = "12px" |
|
p.axis.major_label_standoff = -10 |
|
task = tasks.pop(0) |
|
console.log(f"3/{tasks_total}. {task}") |
|
|
|
# 4. Populate chart... |
|
# The reason for the '+ 1' and 'y - 1' when specifying |
|
# an index value is to avoid O.B.O.B's. Program throws an |
|
# error when y_label[0] is used, can only access from |
|
# '1' or above. column_data starts at 0. So, the index |
|
# (y) needs to drop down 1 from the current 'y' value |
|
# -- which indicates where about in the loop the |
|
# program is regarding the y_labels list. |
|
for y in range (1, (len(y_labels) + 1)): |
|
col = random_colour() |
|
p.line(x_range, |
|
column_data[y - 1], |
|
legend_label=f"{y_labels[y]}", |
|
line_color=random_colour(), |
|
line_width=2) |
|
task = tasks.pop(0) |
|
console.log(f"4/{tasks_total}. {task}") |
|
|
|
# 5. Write chart to disk... |
|
output_file(arguments.output) |
|
save(p) |
|
task = tasks.pop(0) |
|
console.log(f"5/{tasks_total}. {task}") |
|
except IOError: |
|
log.critical("File cannot be found.") |
|
except Exception as e: |
|
log.critical("Unrecoverable system error.") |
|
console.print_exception() |
|
else: |
|
try: |
|
# 1. Load file... |
|
extension = get_extension(arguments.input) |
|
if extension == ".tsv": |
|
data = pd.read_csv(arguments.input, sep="\t", header=None, |
|
index_col=False, dtype='unicode') |
|
elif extension == ".csv": |
|
data = pd.read_csv(arguments.input, sep=",", header=None, |
|
index_col=False, dtype='unicode') |
|
else: |
|
log.critical("File is neither a .csv or .tsv file. Unable to process.") |
|
return |
|
y_labels = get_y_values(data) |
|
data = data[1:] |
|
|
|
# 2. Parse data... |
|
for x in data.columns: |
|
values = data.iloc[:, x] |
|
column_data.append(values) |
|
x_values = column_data[0] |
|
column_data.pop(0) |
|
# If x_values contains duplicated values, they will |
|
# need an 'ID' appended to each value. This part of |
|
# the code checks for them so the code below will know |
|
# to append it or not. If there are no duplicated |
|
# entries, the appendaged is not needed. |
|
dupes = False |
|
for item in x_values.duplicated(): |
|
if item is False: |
|
dupes = True |
|
break |
|
|
|
# Add an 'ID' tag to the x-axis values. This is done |
|
# to stop duplicated values which causes errors in |
|
# Bokeh. |
|
x_range = [] |
|
if dupes == True: |
|
counter = 0 |
|
for row_value in x_values: |
|
x_range.append(f"{counter}: {row_value}") |
|
counter = counter + 1 |
|
else: |
|
x_range = x_values |
|
|
|
# 3. Build chart outline... |
|
p = figure(title=arguments.title, |
|
x_range=FactorRange(* x_range), |
|
x_axis_label=arguments.xaxis, |
|
y_axis_label=arguments.yaxis, |
|
sizing_mode="stretch_both") |
|
p.axis.major_label_text_font_size = "12px" |
|
p.axis.major_label_standoff = 10 |
|
|
|
# 4. Populate chart... |
|
# The reason for the '+ 1' and 'y - 1' when specifying |
|
# an index value is to avoid O.B.O.B's. Program throws an |
|
# error when y_label[0] is used, can only access from |
|
# '1' or above. column_data starts at 0. So, the index |
|
# (y) needs to drop down 1 from the current 'y' value |
|
# -- which indicates where about in the loop the |
|
# program is regarding the y_labels list. |
|
for y in range (1, (len(y_labels) + 1)): |
|
col = random_colour() |
|
p.line(x_range, |
|
column_data[y - 1], |
|
legend_label=f"{y_labels[y]}", |
|
line_color=random_colour(), |
|
line_width=2) |
|
|
|
# 5. Write chart to disk... |
|
output_file(arguments.output) |
|
save(p) |
|
except IOError: |
|
log.critical("File cannot be found.") |
|
except Exception as e: |
|
log.critical("Unrecoverable system error.") |
|
console.print_exception() |
|
|
|
def main(): |
|
args = parse_arguments() |
|
build_chart(args) |
|
|
|
if __name__ == "__main__": |
|
main()
|
|
|