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

#!/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()