diff --git a/src/makegraph/__init__.py b/src/makegraph/__init__.py index d7700ca..852391d 100644 --- a/src/makegraph/__init__.py +++ b/src/makegraph/__init__.py @@ -1 +1,11 @@ -from .makegraph import * \ No newline at end of file +from .colours import ColourValue, ColourContainer +from .uq_colours import UQ_COLOURS_DICT, UQ_COLOURS, add_colours_to_mpl +from .makegraph import makeGraph, pltKeyClose + +__all__ = [ + "makeGraph", "pltKeyClose", + + "ColourValue", "ColourContainer", + + "UQ_COLOURS_DICT", "UQ_COLOURS" +] \ No newline at end of file diff --git a/src/makegraph/__main__.py b/src/makegraph/__main__.py index 6661703..50a633e 100644 --- a/src/makegraph/__main__.py +++ b/src/makegraph/__main__.py @@ -4,7 +4,8 @@ import numpy as np -from . import makeGraph, UQ_COLOURS +from . import makeGraph +from . import UQ_COLOURS if __name__ == '__main__': #This is an example of drawing 4 plots by generating them diff --git a/src/makegraph/colour.py b/src/makegraph/colour.py deleted file mode 100644 index ca8b2b7..0000000 --- a/src/makegraph/colour.py +++ /dev/null @@ -1,66 +0,0 @@ -from functools import lru_cache -import matplotlib.colors as colors - -import numpy as np - -class ColourValue(str): - def __new__(cls, value, /, name=None) -> "ColourValue": - hex_val = colors.to_hex(value, True) - instance = super().__new__(cls, hex_val) - - instance._name = name - - return instance - - def __repr__(self) -> str: - if self.name: - name = f"\"{self.name}\" " - else: - name = "" - - return f"<{self.__class__.__name__}" \ - f" {name}{self} " \ - f"({self.rgba[0]:.2f}, {self.rgba[1]:.2f}, " \ - f"{self.rgba[2]:.2f}, {self.rgba[3]:.2f})>" - - def alpha_adj(self, alpha): - r, g, b, a = self.rgba - return (r, g, b, alpha) - - @property - @lru_cache - def rgba(self) -> tuple[float, float, float, float]: - return colors.to_rgba(self) - - @property - @lru_cache - def rgb(self) -> tuple[float, float, float]: - return colors.to_rgb(self) - - @property - @lru_cache - def hsv(self) -> np.ndarray: - return colors.rgb_to_hsv(self.rgb) - - @property - @lru_cache - def name(self) -> str: - if hasattr(self, "_name") and self._name is not None: - name = self._name - else: - name = "" - - assert isinstance(name, str), \ - TypeError(f"`name` should be an instance of `str` not `{type(name)}`") - - return name - - @property - def hex(self) -> str: - return self - -if __name__ == "__main__": - foo = ColourValue((0,0.5,0.6), "Whoa a name!") - print(foo) - - exit() \ No newline at end of file diff --git a/src/makegraph/colours.py b/src/makegraph/colours.py new file mode 100644 index 0000000..5299466 --- /dev/null +++ b/src/makegraph/colours.py @@ -0,0 +1,177 @@ +# Better Colour Class for matplotlib + +__author__ = "Cal Wing" +__version__ = "0.0.1" + +from cycler import cycler, Cycler +from typing import Sequence, ItemsView, KeysView +from functools import lru_cache +import matplotlib.colors as colors + +import numpy as np + +type ValidColour = ColourValue | \ + str | \ + tuple[float, float, float, float] | \ + tuple[float, float, float] + +class ColourValue(str): + def __new__( + cls, + value: ValidColour, + /, + name: str | None = None + ) -> "ColourValue": + + hex_val = colors.to_hex(value, True) + instance = super().__new__(cls, hex_val) + + instance._name = name + + return instance + + def __repr__(self) -> str: + if self.name: + name = f"\"{self.name}\" " + else: + name = "" + + return f"<{self.__class__.__name__}" \ + f" {name}{self} " \ + f"({self.rgba[0]:.2f}, {self.rgba[1]:.2f}, " \ + f"{self.rgba[2]:.2f}, {self.rgba[3]:.2f})>" + + def alpha_adj(self, alpha): + r, g, b, _ = self.rgba + return (r, g, b, alpha) + + @property + @lru_cache + def rgba(self) -> tuple[float, float, float, float]: + return colors.to_rgba(self) + + @property + @lru_cache + def rgb(self) -> tuple[float, float, float]: + return colors.to_rgb(self) + + @property + @lru_cache + def hsv(self) -> np.ndarray: + return colors.rgb_to_hsv(self.rgb) + + @property + @lru_cache + def name(self) -> str: + if hasattr(self, "_name") and self._name is not None: + name = self._name + else: + name = "" + + assert isinstance(name, str), \ + TypeError(f"`name` should be an instance of `str` not `{type(name)}`") + + return name + + @property + def hex(self) -> str: + return self + + +type ValidColourDicts = dict[str, str | ColourValue] | \ + dict[str, tuple[float, float, float, float]] | \ + dict[str, tuple[float, float, float]] + +class ColourContainer: + def __init__( + self, + colours: ValidColourDicts | Sequence[ColourValue], + *, + lookup_replacement: dict[str, str] = {" ": "_"} + ) -> None: + + self._colours: dict[str, ColourValue] = {} + self._lookup_replacement = lookup_replacement + + if isinstance(colours, dict): + for key, value in colours.items(): + assert isinstance(key, str), \ + TypeError(f"`key` must be an instance of `str` not type `{type(key)}`") + + self._colours[key] = ColourValue(value, name=key) # ty:ignore[invalid-argument-type] + + setattr(self, key, self._colours[key]) + + else: + for colour in colours: + self._colours[colour.name] = colour + + setattr(self, colour.name, self._colours[colour.name]) + + def __repr__(self) -> str: + return f"Colour List of {len(self._colours)} " \ + f"colour{'s' if len(self._colours) > 0 else ''}: " + \ + str(list(self._colours.keys())) + + def __getitem__(self, key: str) -> ColourValue: + for alt_char, original_char in self._lookup_replacement.items(): + if key.replace(alt_char, original_char) in self.keys(): + key = key.replace(alt_char, original_char) + break + + return self._colours[key] + + def __iter__(self): + return iter(self._colours) + + def items(self) -> ItemsView[str, ColourValue]: + return self._colours.items() + + def keys(self) -> KeysView[str]: + return self._colours.keys() + + def make_cycler( + self, + order: Sequence[str] | None = None, + /, + hex: bool = True + ) -> Cycler[str, str]: + + cycle_order = [] + + if order is None: + for _, colour in self.items(): + c = colour.hex if hex else colour + cycle_order.append(c) + else: + for key in order: + c = self[key].hex if hex else self[key] + cycle_order.append(c) + + return cycler(color=cycle_order) + + # Generate a matplotlib colour mapping dictionary + def mpl_colour_mapping( + self, + prefix: str, + /, + hex: bool = True, + *, + add_separator: bool = True + ) -> dict[str, str]: + + mapping: dict[str, str] = {} + + if add_separator: + prefix += ":" + + for name, colour in self.items(): + c = colour.hex if hex else colour + mapping[prefix + name] = c + + # Allow all alt names + for alt_char, original_char in self._lookup_replacement.items(): + mapping[prefix + name.replace(alt_char, original_char)] = c + + return mapping + \ No newline at end of file diff --git a/src/makegraph/makegraph.py b/src/makegraph/makegraph.py index 0e702d7..347c3ef 100644 --- a/src/makegraph/makegraph.py +++ b/src/makegraph/makegraph.py @@ -8,177 +8,13 @@ #### 2025 - Released under MIT __author__ = "Cal Wing" -__version__ = "0.1.12" +__version__ = "0.2.0" -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 @@ -201,7 +37,6 @@ def colorbar(mappable, size="5%", pad=0.05, lsize=None, lpad=None, lax=True, **k 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 diff --git a/src/makegraph/uq_colours.py b/src/makegraph/uq_colours.py new file mode 100644 index 0000000..2a061c5 --- /dev/null +++ b/src/makegraph/uq_colours.py @@ -0,0 +1,138 @@ +# UQ Colour Definitions + +__author__ = "Cal Wing" +__version__ = "0.0.1" + +import matplotlib.pyplot as plt +from .colours import ColourContainer +import matplotlib +import matplotlib.colors as mpl_colors + +# Need to use env-vars to catch if we want to inject colours +import os +_MG_INJECT_MPL_COLOURS_ENVIRON_VAR = "MG_INJECT_MPL_UQ_COLOURS" + +if _MG_INJECT_MPL_COLOURS_ENVIRON_VAR in os.environ: + try: + INJECT_COLOURS = bool(int(os.environ[_MG_INJECT_MPL_COLOURS_ENVIRON_VAR])) + except ValueError: + INJECT_COLOURS = False +else: + # Default to always injecting colours on load + INJECT_COLOURS = True + +# 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" +} + +UQ_COLOURS = ColourContainer(UQ_COLOURS_DICT) + +# Make UQ Colours names for 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_COLOURS.mpl_colour_mapping("uq") + +## 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 = UQ_COLOURS.make_cycler([ + "purple", #51247A -> C00 -> uq:purple + "blue", #4085C6 -> C01 -> uq:blue + "green", #2EA836 -> C02 -> uq:green + "red", #E62645 -> C03 -> uq:red + "light_purple", #962A8B -> C04 -> uq:light_purple + "dark_grey", #999490 -> C05 -> uq:dark_grey + "orange", #EB602B -> C06 -> uq:orange + "yellow", #FBB800 -> C07 -> uq:yellow + "aqua", #00A2C7 -> C08 -> uq:aqua + "gold", #BB9D65 -> C09 -> uq:gold + "neutral" #D7D1CC -> C10 -> uq:neutral +]) + +## 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 = [mpl_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 = mpl_colors.LinearSegmentedColormap('uq', segmentdata=uq_colour_dict) # ty:ignore[invalid-argument-type] + +# Wrap all MPL calls in a function +def add_colours_to_mpl(update_cycler: bool = True, once_only: bool = False): + # Set this env_var so that any subsequent imports inject the correct colours + # oh the joys of multi-processing + if not once_only: + os.environ[_MG_INJECT_MPL_COLOURS_ENVIRON_VAR] = "1" + + # Load "uq:" colours into mpl + mpl_colors.get_named_colors_mapping().update( uq_colour_mapping ) + + # Tell MatPlotLib to use said cycler + # TODO: Make an optional import that tracks + if update_cycler: + plt.rc('axes', prop_cycle=uq_colour_cycler) + + # Add the colour map to MPL + matplotlib.colormaps.register(uq_cmap) + + # Set the colour map - Not a very good default so not doing that + #plt.set_cmap("uq") + +if INJECT_COLOURS: + add_colours_to_mpl() \ No newline at end of file