From 106c0f5b6f6c356e44288929ba2a6475d2735693 Mon Sep 17 00:00:00 2001 From: Cal Wing Date: Fri, 14 Feb 2025 20:11:31 +0100 Subject: [PATCH] My STD deployment --- .gitignore | 163 ++++++++++++ .vscode/launch.json | 16 ++ .vscode/settings.json | 7 + README.md | 2 + debug_lib.py | 97 +++++++ makeGraph.py | 590 ++++++++++++++++++++++++++++++++++++++++++ requirements.txt | Bin 0 -> 494 bytes 7 files changed, 875 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 README.md create mode 100644 debug_lib.py create mode 100644 makeGraph.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..353b5ff --- /dev/null +++ b/.gitignore @@ -0,0 +1,163 @@ +# ---> Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +#test_* diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..902091f --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Current File", + "type": "python", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal", + "args": ["--debug", "--data-path", "./data"] + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..8e9d364 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "[python]": {"editor.rulers": [80, 100, 120]}, + "files.exclude": { + ".venv": true, + "__pycache__": true + } +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..caf3623 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# ENGG1001_A1_S1_2025 + diff --git a/debug_lib.py b/debug_lib.py new file mode 100644 index 0000000..7ee510a --- /dev/null +++ b/debug_lib.py @@ -0,0 +1,97 @@ +""" debug_lib + A bunch of helpful debuging functions + Cal.W 2019 +""" +class auto_input: + """Given a list automagicly enter a value.""" + def __init__(self, input_list): + #Just setup some internal stuff + self.input_index = 0 + self.input_list = input_list + + def __call__(self, input_text): + #TLDR: Get the next value from the array and return that + # If there was an error then ask the user for input + try: + out = self.input_list[self.input_index] + except: + out = None + + self.input_index += 1 + + if out is None: + out = __builtins__['input']('#Input#: '+input_text) + else: + print(str(input_text)+str(out)) + + return str(out) + +def stringCompairTests(test_function, tests, doPrint=True, logFile=None): + """Run a bunch of tests on a function and check the outputs via comparing them to wanted outputs as strings. + Prams: + test_function: function - This needs to be the function to test + tests: list - The list of tests to run. Each test is a list in the form: [(arg1, arg2, ...), expected_output] + Note: The number of arguments in the tuple *must* equal the number required for the function + doPrint: bool - Print the test results - Default: True + logFile: str - The path to write the output as a log if it is NoneType(None) then it won't make a logfile - Default: None + + Returns: + list - A list containing each test result as a tuple in the form: (returned_result, Bool(test_succeeded), test) + + + Example: + def doThing(arg1, arg2): + return arg1 + arg2 + + tests = [ + [(1, 2), 3], + [("Hello", "World"), "HelloWorld"], + [(1, 3), 2] + ] + + test_results = stringCompairTests(doThing, tests) + + >>>test_results => [ + (3, True, [(1, 2), 3]), + ("HelloWorld", True, [("Hello", "World"), "HelloWorld"]), + (4, False, [(1, 3), 2]) + ] + """ + test_results = [] + logString = "" + + for i, test in enumerate(tests): + result = test_function(**test[0]) if type(test[0]) is dict else test_function(*test[0]) + test_results.append((result, str(test[1]) == str(result), test)) + + logStringL = "-"*50 + logStringL += "\n"+ "Test: " + str(i) + (" - Success" if result else " - Failure") + logStringL += "\n"+ "Given Arguments: \"" + '", "'.join(test[0]) + '"' + logStringL += "\n"+ "Expected Output: " + str(test[1]) + logStringL += "\n"+ "Returned Output: " + str(result) + logStringL += "\n"+ "Returned Values " + ("don't" if not result else "") + "match." + + logString += '\n' + logStringL + + if doPrint: print(logStringL) + + + successfull_tests = sum([1 for x in test_results if x[1]]) + logStringL = "\n" + str(successfull_tests) + " test" + ("s" if len(test_results)-successfull_tests == 1 else '') + logStringL += " succeeded with " + str(len(test_results)-successfull_tests) + " test" + logStringL += ("s" if len(test_results)-successfull_tests == 1 else "") + " failing." + logString += '\n' + logStringL + + if doPrint: print(logStringL,"\n") + + if logFile not in (None, ""): + if type(logFile) is bool and logFile: logFile = __file__.rsplit('\\', 1)[1] + if logString[0] == "\n": logString = logString[1:] + + with open(str(logFile)+".log", 'w') as writeFile: + writeFile.write(logString) + writeFile.close() + + if doPrint: print("The logfile:", '\''+str(logFile)+".log'", "has been written to.", "\n") + + return test_results \ No newline at end of file diff --git a/makeGraph.py b/makeGraph.py new file mode 100644 index 0000000..4f9ff79 --- /dev/null +++ b/makeGraph.py @@ -0,0 +1,590 @@ +### MatPlotLib Graph Wrapper +#### Written by Cal.W 2020, originally for MECH2700 but continually +#### expanded upon. +#### 2023 - Added UQ Colors +#### 2023 - Added pltKeyClose function +#### 2023 - Added UQ Default Colours to MatPlotLib +#### 2023 - Added auto-magic 3D plot generation, label addition & min/max line points + +__author__ = "Cal Wing" +__version__ = "0.1.12" + +from collections.abc import Iterator +import numpy as np +import matplotlib +import matplotlib.pyplot as plt +import matplotlib.colors as colors +from mpl_toolkits.axes_grid1 import make_axes_locatable +from cycler import cycler +#from tqdm import tqdm +from collections import UserString + +# Define the UQ Colours +UQ_COLOURS_DICT = { + # Primary + "purple": "#51247A", + "white" : "#FFFFFF", + "black" : "#000000", + + # Secondary + "light_purple": "#962A8B", + "red" : "#E62645", + "green" : "#2EA836", + "gold" : "#BB9D65", + "neutral" : "#D7D1CC", + "orange" : "#EB602B", + "yellow" : "#FBB800", + "blue" : "#4085C6", + "aqua" : "#00A2C7", + "dark_grey" : "#999490" +} + + +# Define a colour object that can do neat conversions & things, by default stores as hex value +class ColourValue(UserString): + def __init__(self, name, value): + self.name = name + self.value = colors.to_hex(value, True) + super().__init__(self.value) + + #def __new__(self, name, value): + # self.name = name + # self.value = colors.to_hex(value, True) + # + # return super().__new__(self, self.value) + + def __str__(self) -> str: + return self.value + + def __repr__(self) -> str: + return self.name + " " + self.value + " " + str(self.rgba()) + + def rgba(self) -> tuple[float, float, float, float]: + return colors.to_rgba(self.value) + + def rgb(self) -> tuple[float, float, float]: + return colors.to_rgb() + + def hex(self) -> str: + return self.value + + def hsv(self) -> np.ndarray: + return colors.rgb_to_hsv(self.rgb()) + + def alpha_adj(self, alpha): + r, g, b, a = self.rgba() + return (r, g, b, alpha) + +# Define the UQ Colours in a nicer object +class ColourList(object): + def __init__(self, colours: dict) -> None: + self.colours = {} + + for key, value in colours.items(): + self.colours[key] = ColourValue(key, value) + + setattr(self, key, self.colours[key]) + + def __getitem__(self, key: str) -> str: + if key.replace(" ", "_") in self.colours.keys(): + key = key.replace(" ", "_") + + return self.colours[key] + + def items(self): + return self.colours.items() + + def __repr__(self) -> str: + return f"Colour List of {len(self.colours)} colour{'s' if len(self.colours) > 0 else ''}: " + str(list(self.colours.keys())) + +UQ_COLOURS = ColourList(UQ_COLOURS_DICT) + +# Load UQ Colours into MatPlotLib +# UQ colours are prefaced with 'uq:', so UQ red is 'uq:red' +# Note: Any names That have a _ also have a version with spaces so both "uq:light_purple" and "uq:light purple" work +uq_colour_mapping = {'uq:' + name: str(value) for name, value in list(UQ_COLOURS.items()) + [(x[0].replace("_", " "), x[1]) for x in UQ_COLOURS.items() if "_" in x[0]]} +colors.get_named_colors_mapping().update( uq_colour_mapping ) + +## UQ Colour Cycler +# +-----------------------------+-----------------------------+ +# | Default (Tab) | UQ | +# +-----------------------------+-----------------------------+ +# | C00 | #1f77b4 -> tab:blue | #51247A -> uq:purple | +# | C01 | #ff7f0e -> tab:orange | #4085C6 -> uq:blue | +# | C02 | #2ca02c -> tab:green | #2EA836 -> uq:green | +# | C03 | #d62728 -> tab:red | #E62645 -> uq:red | +# | C04 | #9467bd -> tab:purple | #962A8B -> uq:light_purple | +# | C05 | #8c564b -> tab:brown | #999490 -> uq:dark_grey | +# | C06 | #e377c2 -> tab:pink | #EB602B -> uq:orange | +# | C07 | #7f7f7f -> tab:grey | #FBB800 -> uq:yellow | +# | C08 | #bcbd22 -> tab:olive | #00A2C7 -> uq:aqua | +# | C09 | #17becf -> tab:cyan | #BB9D65 -> uq:gold | +# | C10 | | #D7D1CC -> uq:neutral | +# +-----------------------------+-----------------------------+ + +# Build a colour cycler +uq_colour_cycler = cycler(color=[ + UQ_COLOURS["purple"].data, #51247A -> C00 -> uq:purple + UQ_COLOURS["blue"].data, #4085C6 -> C01 -> uq:blue + UQ_COLOURS["green"].data, #2EA836 -> C02 -> uq:green + UQ_COLOURS["red"].data, #E62645 -> C03 -> uq:red + UQ_COLOURS["light_purple"].data, #962A8B -> C04 -> uq:light_purple + UQ_COLOURS["dark_grey"].data, #999490 -> C05 -> uq:dark_grey + UQ_COLOURS["orange"].data, #EB602B -> C06 -> uq:orange + UQ_COLOURS["yellow"].data, #FBB800 -> C07 -> uq:yellow + UQ_COLOURS["aqua"].data, #00A2C7 -> C08 -> uq:aqua + UQ_COLOURS["gold"].data, #BB9D65 -> C09 -> uq:gold + UQ_COLOURS["neutral"].data #D7D1CC -> C10 -> uq:neutral +]) + +# Tell MatPlotLib to use said cycler +plt.rc('axes', prop_cycle=uq_colour_cycler) + + +## UQ Colour Gradient (Not very good :( ) +uq_colour_map_grad = [ + UQ_COLOURS["purple"], + UQ_COLOURS["light_purple"], + UQ_COLOURS["light_purple"], + UQ_COLOURS["blue"], + UQ_COLOURS["blue"], + UQ_COLOURS["aqua"], + UQ_COLOURS["green"], + UQ_COLOURS["green"], + UQ_COLOURS["green"], + UQ_COLOURS["yellow"], + UQ_COLOURS["yellow"] +] + +#Convert to RGB values +uq_colour_map_grad = [colors.to_rgb(c) for c in uq_colour_map_grad] + +#Populate the working dict +uq_colour_dict = { + "red": [], + "green": [], + "blue": [], +} +for i, c in enumerate(uq_colour_map_grad): + offset = i / (len(uq_colour_map_grad) - 1) + uq_colour_dict["red"].append( (offset, c[0], c[0]) ) + uq_colour_dict["green"].append( (offset, c[1], c[1]) ) + uq_colour_dict["blue"].append( (offset, c[2], c[2]) ) + +#Define & register the colour map itself +uq_cmap = colors.LinearSegmentedColormap('uq',segmentdata=uq_colour_dict) +matplotlib.colormaps.register(uq_cmap) + +# Set the colour map - Not a very good default so not doing that +#plt.set_cmap("uq") + + +## Colorbar Function by Joseph Long & Mike Lampton +# Retrieved from https://joseph-long.com/writing/colorbars/ on 31/10/2021 +# Minor Modifications made by Cal.W 2021 +def colorbar(mappable, size="5%", pad=0.05, lsize=None, lpad=None, lax=True, **kwargs): + last_axes = plt.gca() + ax = mappable.axes + fig = ax.figure + divider = make_axes_locatable(ax) + if lax: + lsize = lsize if lsize is not None else size + lpad = lpad if lpad is not None else pad + dax = divider.append_axes("left", size=lsize, pad=lpad) + dax.set_frame_on(False) + dax.grid(False) + dax.set_yticks([]) + dax.set_xticks([]) + cax = divider.append_axes("right", size=size, pad=pad) + cbar = fig.colorbar(mappable, cax=cax, **kwargs) + plt.sca(last_axes) + return cbar + + +## Make Graph Function +def makeGraph(graphData, showPlot=True, doProgramBlock=True, figSavePath=None, hideEmptyAxis=False) -> tuple[matplotlib.figure.Figure, tuple[matplotlib.axes.Axes, ...]]: + """ Generate a matplotlib graph based on a simple dictionary object + Input: + dict(graphData): The dictionary containing all the graph data - see example for more info + bool(showPlot[True]): Should the function display the plot + bool(doProgramBlock[True]): Should the function block the main python thread + str(figSavePath[None]): The path to save a copy of the figure, calls fig.savefig if not None + + Returns: The the figure and axes from matplotlib.pyplot.subplots() + From 'matplotlib.pyplot.subplots(): + fig : `matplotlib.figure.Figure` + ax : `matplotlib.axes.Axes` or array of Axes + *ax* can be either a single `~matplotlib.axes.Axes` object or an + array of Axes objects if more than one subplot was created. + Example: + makeGraph({ + "title": "Simple Plot", + "xLabel": "x label", + "yLabel": "y label", + "plots": [ + {"x":[0,1,2,3,4], "y":[0,1,2,3,4], "label":"Linear"}, + {"x":[0,1,2,3,4], "y":[5,5,5,5,5]}, + {"x":[4,3,2,1,0], "y":[4,3,2,1,0], "label":"Linear2"}, + {"x":0, "type":"axvLine", "label":"Red Vertical Line", "color":"red"}, + {"y":6, "type":"axhLine", "label":"Dashed Horizontal Line", "args":{"linestyle":"--"}}, + {"type":"scatter", "x":4, "y":4, "label":"A Random Point", "colour":"purple", "args":{"zorder":2}} + ] + }) + """ + + doKeyCopy = True + plotDim = (1,) + if "subPlots" in graphData: + if "plotDim" in graphData: plotDim = graphData["plotDim"] + else: plotDim = (1,len(graphData["subPlots"])) + else: + graphData["subPlots"] = [graphData] + doKeyCopy = False + + figSize = graphData["figSize"] if "figSize" in graphData else None + tightLayout = graphData["tightLayout"] if "tightLayout" in graphData else True + + fig, axes = plt.subplots(*plotDim, figsize=figSize) # Create a figure and an axes. + + #if len(graphData["subPlots"]) <= 1: + # axes = [axes] + #Makes everything nice and linear + # IE ((1,2), (3,4)) = (1,2,3,4) + flatAxes = np.array(axes).flatten().tolist() + + loopKeys = [ + "xLabel", "yLabel", "title", "axis", "grid", "xPos", "yPos", + "xLabelPos", "yLabelPos", "xTickPos", "yTickPos", "xScale", "yScale", + "xTickMap", "yTickMap", "plots", "xLim", "yLim", "ledgLoc", "y2Label", + "ticklabel", "xInvert", "yInvert" + ] + + #Feel like this could be optimized + if doKeyCopy: + for key in loopKeys: + if key not in graphData: continue + if key in graphData: + for axGraphData in graphData["subPlots"]: + if key not in axGraphData: + axGraphData[key] = graphData[key] + + + for i, axGraphData in enumerate(graphData["subPlots"]): + ax1 = flatAxes[i] + if "skip" in axGraphData and axGraphData["skip"]: + ax1.set_axis_off() + continue + + if bool(sum([("y2" in pData) for pData in axGraphData["plots"]])): + ax2 = ax1.twinx() + else: + ax2 = None + + additional_legends = [] + + if bool(sum([("z" in pData) for pData in axGraphData["plots"]])): + orig_ax = ax1 + ax1 = fig.add_subplot(orig_ax.get_position(), projection='3d') + orig_ax.remove() + if "tightLayout" not in graphData: tightLayout = False + + # Duct Tape + ax = ax1 + + #Draw many plots as needed + # Also provide functions for drawing other types of lines + if "plots" in axGraphData: + for pData in axGraphData["plots"]: + getSafeValue = lambda key, result=None: pData[key] if key in pData else result #Only return the key-value if present in pData + getSafeValue2 = lambda key, key2, result=None: pData[key][key2] if key in pData and key2 in pData[key] else result + getSafeColour = getSafeValue("colour") or getSafeValue("color") #Frigen American Spelling + optArgs = getSafeValue("args", {}) #Allow for other args to be passed in + + currentLine = None + + if "skip" in pData and pData["skip"]: continue + + if "x" in pData: + xData = pData["x"] + + if "y" in pData: + yData = pData["y"] + elif "y2" in pData: + yData = pData["y2"] + ax = ax2 + + zData = pData["z"] if "z" in pData else None + + if "type" not in pData or pData["type"] == "plot": + if zData is not None: + currentLine = ax.plot(xData, yData, zData, label=getSafeValue("label"), color=getSafeColour, **optArgs) + else: + currentLine = ax.plot(xData, yData, label=getSafeValue("label"), color=getSafeColour, **optArgs) + currentLine = currentLine[0] + currentLineColour = currentLine.get_color() + + if "maxPoint" in pData: + labelText = pData["maxPoint"] if type(pData["maxPoint"]) == str else "Maximum Point ({y:.2f})" + x, y = np.array(xData), np.array(yData) + maxPoint = x[np.argmax(y)], np.max(y) + ax.scatter(maxPoint[0], maxPoint[1], + marker=getSafeValue("maxMarker", "o"), label=labelText.format(*maxPoint, x=maxPoint[0], y=maxPoint[1]), + color=currentLineColour, zorder=getSafeValue("maxZorder", 2)) + + if "minPoint" in pData: + labelText = pData["minPoint"] if type(pData["minPoint"]) == str else "Minimum Point ({1:.2f})" + x, y = np.array(xData), np.array(yData) + minPoint = x[np.argmin(y)], np.min(y) + ax.scatter(minPoint[0], minPoint[1], + marker=getSafeValue("minMarker", "*" if "maxPoint" in pData else "o"), + label=labelText.format(*minPoint, x=minPoint[0], y=minPoint[1]), color=currentLineColour, + zorder=getSafeValue("minxZorder", 2)) + + + + elif pData["type"] == "point": + ax.scatter(xData, yData, + marker=getSafeValue("marker"), label=getSafeValue("label"), + color=getSafeColour, zorder=getSafeValue("zorder", 2), + **optArgs) + elif pData["type"] == "hLine": + currentLine = ax.hlines(yData, *xData, label=getSafeValue("label"), color=getSafeColour, **optArgs) + elif pData["type"] == "vLine": + currentLine = ax.vlines(xData, *yData, label=getSafeValue("label"), color=getSafeColour, **optArgs) + elif pData["type"] == "axvLine": + if "y" not in pData: yData = (0, 1) #Span the whole graph + currentLine = ax.axvline(xData, *yData, label=getSafeValue("label"), color=getSafeColour, **optArgs) + elif pData["type"] == "axhLine": + if "x" not in pData: xData = (0, 1) #Span the whole graph + currentLine = ax.axhline(yData, *xData, label=getSafeValue("label"), color=getSafeColour, **optArgs) + elif pData["type"] == "scatter": + ax.scatter(xData, yData, marker=getSafeValue("marker"), label=getSafeValue("label"), color=getSafeColour, **optArgs) + + elif pData["type"] == "contour": + cs = ax.contour(getSafeValue("x"), getSafeValue("y"), pData["z"], levels=getSafeValue("levels"), colors=getSafeColour, **optArgs) + if "label" in pData: cs.collections[0].set_label(getSafeValue("label")) + elif pData["type"] == "matshow": + ms = ax.matshow(pData["matrix"], origin=getSafeValue("origin"), label=getSafeValue("label"), **optArgs) + if "colourBar" in pData: + colorbar(ms, extend=getSafeValue2("colourBar", "extend")) + elif pData["type"] == "pColourMesh": + mesh = [] + if "X" in pData or "Y" in pData: + mesh = [xData, yData, pData["Z"]] + if "x" in pData or "y" in pData: + x = xData; y = yData + + if type(x) in [int, float]: x = (0, x, None) + if type(y) in [int, float]: y = (0, x, None) + x = tuple(x); y = tuple(y) + if len(x) < 3: x = (x[0], x[1], None) + if len(y) < 3: y = (y[0], y[1], None) + + x = np.arange(x[0], x[1], x[2]) + y = np.arange(y[0], y[1], y[2]) + X, Y = np.meshgrid(x, y) + + mesh = [X, Y, pData["Z"]] + else: + mesh = [pData["Z"]] + + cNorm = None + if "norm" in pData: + cNorm = colors.LogNorm(vmin=pData["norm"][0], vmax=pData["norm"][1]) + + pcMesh = ax.pcolormesh(*mesh, norm=cNorm, shading=getSafeValue("shading"), label=getSafeValue("label"), **optArgs) + #pcMesh = ax.imshow(pData["Z"], norm=cNorm, origin="lower") + + if "colourBar" in pData: + cBarOptArgs = pData["colourBar"]["optArgs"] if "optArgs" in pData["colourBar"] else {} + fig.colorbar(pcMesh, ax=ax, extend=getSafeValue2("colourBar", "extend"), **cBarOptArgs) + elif pData["type"] == "imshow": + cNorm = None + if "norm" in pData: + cNorm = colors.LogNorm(vmin=pData["norm"][0], vmax=pData["norm"][1]) + + ims = ax.imshow(pData["data"], norm=cNorm, origin=getSafeValue("origin"), label=getSafeValue("label"), **optArgs) + + if "colourBar" in pData: + cBarOptArgs = pData["colourBar"]["optArgs"] if "optArgs" in pData["colourBar"] else {} + colorbar(ims, extend=getSafeValue2("colourBar", "extend"), **cBarOptArgs) + elif pData["type"] == "text": + if not "props" in pData: + props = { + "boxstyle" : getSafeValue("boxstyle", "round"), + "facecolor": getSafeValue("facecolor", getSafeValue("facecolour", "wheat")), + "alpha" : getSafeValue("alpha", 0.5) + } + + align = ( + getSafeValue("valign", None), + getSafeValue("halign", None), + ) + align = getSafeValue("align", align) + + ax.text(getSafeValue("x", 0.05), getSafeValue("y", 0.95), pData["text"], transform=ax.transAxes, fontsize=getSafeValue("fontsize", None), va=align[0], ha=align[1], bbox=props) + elif pData["type"] == "addLabel": + additional_legends.append(pData["line"]) + + if "fillAlpha" in pData or ("fill" in pData and ("type" not in pData or pData["type"] in ["plot"])): + currentLine = ax.get_lines()[-1] if currentLine is None else currentLine + + fillData = pData["fill"] if type(pData["fill"]) == dict and "fill" in pData else {} + + if "color" not in fillData: + line_colour = colors.to_rgba(currentLine.get_color()) + + if "fillAlpha" not in fillData: fillData["fillAlpha"] = pData["fillAlpha"] if "fillAlpha" in pData else 0.07 + line_colour = (line_colour[0], line_colour[1], line_colour[2], fillData["fillAlpha"]) + + fillData["color"] = line_colour + + if "y2" not in fillData: fillData["y2"] = 0 + if "where" not in fillData: fillData["where"] = None + if "interpolate" not in fillData: fillData["interpolate"] = False + if "step" not in fillData: fillData["step"] = None + if "data" not in fillData: fillData["data"] = None + if "args" not in fillData: fillData["args"] = {} + + ax.fill_between(xData, yData, fillData["y2"], color=fillData["color"], + where=fillData["where"], interpolate=fillData["interpolate"], + step=fillData["step"], data=fillData["data"], **fillData["args"] + ) + + #Set extra options as needed + ax = ax1 + if "xLabel" in axGraphData: ax.set_xlabel(axGraphData["xLabel"]) # Add an x-label to the axes. + if "yLabel" in axGraphData: ax.set_ylabel(axGraphData["yLabel"]) # Add an y-label to the axes. + if "zLabel" in axGraphData: ax.set_zlabel(axGraphData["zLabel"]) # Add a y2-label to the axes. + if "y2Label" in axGraphData: ax2.set_ylabel(axGraphData["y2Label"]) # Add a y2-label to the axes. + if "title" in axGraphData: ax.set_title(axGraphData["title"]) # Add an title to the axes. + if "axis" in axGraphData: ax.axis(axGraphData["axis"]) # Set the axis type + if "grid" in axGraphData: ax.grid(axGraphData["grid"]) # Add grids to the graph + if "xPos" in axGraphData: # Add the abilty to move the x axis label and ticks + ax.xaxis.set_label_position(axGraphData["xPos"]) + ax.xaxis.set_ticks_position(axGraphData["xPos"]) + if "yPos" in axGraphData: # Add the abilty to move the y axis label and ticks + ax.yaxis.set_label_position(axGraphData["yPos"]) + ax.yaxis.set_ticks_position(axGraphData["yPos"]) + if "xLabelPos" in axGraphData: ax.xaxis.set_label_position(axGraphData["xLabelPos"]) # Add the ability to move the x axis label + if "yLabelPos" in axGraphData: ax.yaxis.set_label_position(axGraphData["yLabelPos"]) # Add the ability to move the y axis label + if "xTickPos" in axGraphData: ax.xaxis.set_ticks_position(axGraphData["xTickPos"]) # Add the ability to move the x axis ticks + if "yTickPos" in axGraphData: ax.yaxis.set_ticks_position(axGraphData["yTickPos"]) # Add the ability to move the y axis ticks + if "xScale" in axGraphData: ax.set_xscale(axGraphData["xScale"]) #Add x axis scaling if needed + if "yScale" in axGraphData: ax.set_yscale(axGraphData["yScale"]) #Add y axis scaling if needed + if "xInvert" in axGraphData and axGraphData["xInvert"]: ax.invert_xaxis() + if "yInvert" in axGraphData and axGraphData["yInvert"]: ax.invert_yaxis() + if "xLim" in axGraphData: + xLimit = () + if type(axGraphData["xLim"]) in [int, float]: + xLimit = (0, axGraphData["xLim"]) + else: + xLimit = axGraphData["xLim"] + ax.set_xlim(xLimit) + if "yLim" in axGraphData: + yLimit = () + if type(axGraphData["yLim"]) in [int, float]: + yLimit = (0, axGraphData["yLim"]) + else: + yLimit = axGraphData["yLim"] + ax.set_ylim(yLimit) + if "xTickMap" in axGraphData: #Allow for the mapping / transformation of the xAxis Ticks + formatter = axGraphData["xTickMap"] + xTicks = matplotlib.ticker.FuncFormatter(formatter) + ax.xaxis.set_major_formatter(xTicks) + if "yTickMap" in axGraphData: #Allow for the mapping / transformation of the yAxis Ticks + formatter = axGraphData["yTickMap"] + yTicks = matplotlib.ticker.FuncFormatter(formatter) + ax.yaxis.set_major_formatter(yTicks) + + if "plots" in axGraphData and bool(sum([("label" in pData) for pData in axGraphData["plots"]])) and not ("noLedg" in axGraphData and axGraphData["noLedg"]) : + locPoint = axGraphData["ledgLoc"] if "ledgLoc" in axGraphData else None + add_lines, add_labels = additional_legends, [line.get_label() for line in additional_legends] + lines1, labels1 = ax1.get_legend_handles_labels() + + if ax2: + lines2, labels2 = ax2.get_legend_handles_labels() + ax2.legend(lines1 + lines2 + add_lines, labels1 + labels2 + add_labels, loc=locPoint) + else: + ax1.legend(lines1 + add_lines, labels1 + add_labels, loc=locPoint) + + if "ticklabel" in axGraphData: + style = axGraphData["ticklabel"]["style"] if "style" in axGraphData["ticklabel"] else "" + axis = axGraphData["ticklabel"]["axis"] if "axis" in axGraphData["ticklabel"] else "both" + limits = axGraphData["ticklabel"]["limits"] if "limits" in axGraphData["ticklabel"] else None + optArgs = axGraphData["ticklabel"]["optArgs"] if "optArgs" in axGraphData["ticklabel"] else {} + ax.ticklabel_format(axis=axis, style=style, scilimits=limits, **optArgs) + + #Should work? + if hideEmptyAxis: + if not ax.collections and not ax.lines: + ax.set_axis_off() + + + if "title" in graphData or "winTitle" in graphData: + title = graphData["winTitle"] if "winTitle" in graphData else graphData["title"] + fig.canvas.manager.set_window_title(title.replace("\n", " ")) #Set the figure title correctly + if "figTitle" in graphData: + getSafeValue = lambda key: graphData[key] if key in graphData else None #Only return the key-value if present in graphData + + fig.suptitle(graphData["figTitle"], fontsize=getSafeValue("figTitleFontSize")) + if "title" not in graphData: fig.canvas.manager.set_window_title(graphData["figTitle"].replace("\n", " ")) + + if tightLayout: + fig.tight_layout() #Fix labels being cut off sometimes + + #Very big hack + if hideEmptyAxis: + flatAxes[-1].set_axis_off() + + if figSavePath: + fig.savefig(figSavePath) + + if showPlot: + plt.show(block=doProgramBlock) #Show the plot and also block the program - doing things OO style allow for more flexible programs + + + return fig, axes + +# [TODO] Make this Async so the closure of all graphs exits +def pltKeyClose(): + '''Show all plots and wait for user input to close them all.''' + plt.show(block=False) + input('Press any key to close all graphs...') + plt.close() + +if __name__ == '__main__': + #This is an example of drawing 4 plots by generating them + graphData = { + "figTitle": "Simple Plot", + "figTitleFontSize": 16, + "figSize": (8,8), #Yay America, this is in inches :/ # Note: cm = 1/2.54 + "xLabel": "x label", + "yLabel": "y label", + "plotDim": (2,2), + "subPlots":[] + } + + #Create 4 identical plots with different names + for i in range(4): + newPlot = { + "title": f"Graph {i+1}", + "noLedg": i % 2 == 0, + "plots": [ + {"x":[0,1,2,3,4], "y":[0,1,2,3,4], "label":"Linear"}, + {"x":[0,1,2,3,4], "y":[5,5,5,5,5]}, + {"x":[4,3,2,1,0], "y":[4,3,2,1,0], "label":"Linear2"}, + {"x":0, "type":"axvLine", "label":"Red Vertical Line", "color":"uq:red"}, + {"y":6, "type":"axhLine", "label":"Dashed Horizontal Line", "args":{"linestyle":"--"}}, + {"type":"point", "x":4, "y":4, "label":"A Random Point", "colour":"uq:purple"}, + { + "x":np.arange(0, 4, 0.1), "y": 3*np.sin(np.arange(0, 4, 0.1)), + "colour":"uq:dark_grey", "maxPoint": True, + "minPoint": "Custom Min {x:.1f}, {y:.1f}", + "fill": {"color": UQ_COLOURS["green"].alpha_adj(0.07)} + } + ] + } + graphData["subPlots"].append(newPlot) + + makeGraph(graphData) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8be09de931ba1dac5dfea9f6801bbfab7c021193 GIT binary patch literal 494 zcmZ9J$qs@*5Jc;2;-^SN(dfaCaX}?)3gg1ht5rQCL_@kSUGu8CdB66kQLq|pQL;At zrihTRmKb9MDJnSj8&wx{rinS9v8F-OQCA?P;!B8a9^3pF7^29pHI+OPtb0n13=dXo zntM-^m$VCGDWzi$rFQHYc1D|1v0}|lRWWV(mv(6!?Q|hykDUF)_Z7QR&-hkbc|}IH zJRUZS?JC2@x`QYU(+&i^S6aWdG!ADnDok*&+$nQ8z9@oY6!woutI2ZVqAl}fKl%MV Z{k`S+47uc{&HTjSN~h9h{vQroe*p8lNPGYQ literal 0 HcmV?d00001