From 42a9bf2eda2eed8620bd2867a8b6a334e1ef0001 Mon Sep 17 00:00:00 2001 From: Nikhil Bhavikatti <36807179+nikhilbhavikatti@users.noreply.github.com> Date: Fri, 25 Oct 2024 11:43:29 +0200 Subject: [PATCH] Set Default colors for all the plots (#27) * Set Default colors for all the plots * Add distribution colors and colorblind safe option for high dimensional plots --- uadapy/plotting/plots1D.py | 27 +++++---- uadapy/plotting/plots2D.py | 112 ++++++++++++++++++++++++++++--------- uadapy/plotting/plotsND.py | 105 ++++++++++++++++++++++++++++++---- uadapy/plotting/utils.py | 53 +++++++++++++++++- 4 files changed, 247 insertions(+), 50 deletions(-) diff --git a/uadapy/plotting/plots1D.py b/uadapy/plotting/plots1D.py index 4bbaab4..a9fa293 100644 --- a/uadapy/plotting/plots1D.py +++ b/uadapy/plotting/plots1D.py @@ -5,6 +5,7 @@ import glasbey as gb import seaborn as sns from matplotlib.patches import Ellipse +import uadapy.plotting.utils as utils def _calculate_freedman_diaconis_bins(data): @@ -42,7 +43,7 @@ def _setup_plot(distributions, n_samples, seed, fig=None, axs=None, colors=None, axs : matplotlib.axes.Axes or array of Axes or None, optional Axes object(s) to use for plotting. If None, new axes will be created. colors : list or None, optional - List of colors to use for each distribution. If None, Glasbey colors will be used. + List of colors to use for each distribution. If None, Matplotlib Set2 and glasbey colors will be used. colorblind_safe : bool, optional If True, the plot will use colors suitable for colorblind individuals. Default is False. @@ -96,13 +97,19 @@ def _setup_plot(distributions, n_samples, seed, fig=None, axs=None, colors=None, for d in distributions: samples.append(d.sample(n_samples, seed)) - # Generate Glasbey colors + # Generate colors if colors is None: - palette = gb.create_palette(palette_size=len(samples), colorblind_safe=colorblind_safe) + if colorblind_safe: + palette = gb.create_palette(palette_size=len(samples), colorblind_safe=colorblind_safe) + else: + palette = utils.get_colors(len(samples)) else: # If colors are provided but fewer than the number of samples, add more colors from Glasbey palette if len(colors) < len(samples): - additional_colors = gb.create_palette(palette_size=len(samples) - len(colors), colorblind_safe=colorblind_safe) + if colorblind_safe: + additional_colors = gb.create_palette(palette_size=len(samples) - len(colors), colorblind_safe=colorblind_safe) + else: + additional_colors = utils.get_colors(len(samples) - len(colors)) colors.extend(additional_colors) palette = colors @@ -145,7 +152,7 @@ def plot_1d_distribution( dim_labels : list or None, optional Titles for each subplot. distrib_colors : list or None, optional - List of colors to use for each distribution. If None, Glasbey colors will be used. + List of colors to use for each distribution. If None, Matplotlib Set2 and glasbey colors will be used. vert : bool, optional If True, plots will be drawn vertically. If False, plots will be drawn horizontally. Default is True. @@ -336,7 +343,7 @@ def generate_boxplot(distributions, dim_labels : list or None, optional Titles for each subplot. distrib_colors : list or None, optional - List of colors to use for each distribution. If None, Glasbey colors will be used. + List of colors to use for each distribution. If None, Matplotlib Set2 and glasbey colors will be used. vert : bool, optional If True, plots will be drawn vertically. If False, plots will be drawn horizontally. Default is True. @@ -395,7 +402,7 @@ def generate_violinplot(distributions, dim_labels : list or None, optional Titles for each subplot. distrib_colors : list or None, optional - List of colors to use for each distribution. If None, Glasbey colors will be used. + List of colors to use for each distribution. If None, Matplotlib Set2 and glasbey colors will be used. vert : bool, optional If True, plots will be drawn vertically. If False, plots will be drawn horizontally. Default is True. @@ -451,7 +458,7 @@ def generate_dotplot(distributions, dim_labels : list or None, optional Titles for each subplot. distrib_colors : list or None, optional - List of colors to use for each distribution. If None, Glasbey colors will be used. + List of colors to use for each distribution. If None, Matplotlib Set2 and glasbey colors will be used. vert : bool, optional If True, plots will be drawn vertically. If False, plots will be drawn horizontally. Default is True. @@ -511,7 +518,7 @@ def generate_stripplot(distributions, dim_labels : list or None, optional Titles for each subplot. distrib_colors : list or None, optional - List of colors to use for each distribution. If None, Glasbey colors will be used. + List of colors to use for each distribution. If None, Matplotlib Set2 and glasbey colors will be used. vert : bool, optional If True, plots will be drawn vertically. If False, plots will be drawn horizontally. Default is True. @@ -571,7 +578,7 @@ def generate_swarmplot(distributions, dim_labels : list or None, optional Titles for each subplot. distrib_colors : list or None, optional - List of colors to use for each distribution. If None, Glasbey colors will be used. + List of colors to use for each distribution. If None, Matplotlib Set2 and glasbey colors will be used. vert : bool, optional If True, plots will be drawn vertically. If False, plots will be drawn horizontally. Default is True. diff --git a/uadapy/plotting/plots2D.py b/uadapy/plotting/plots2D.py index 4ad19fb..b205df4 100644 --- a/uadapy/plotting/plots2D.py +++ b/uadapy/plotting/plots2D.py @@ -1,10 +1,19 @@ import matplotlib.pyplot as plt import numpy as np from uadapy import Distribution -from numpy import ma -from matplotlib import ticker - -def plot_samples(distributions, n_samples, seed=55, xlabel=None, ylabel=None, title=None, show_plot=False): +from matplotlib.colors import ListedColormap +import uadapy.plotting.utils as utils +import glasbey as gb + +def plot_samples(distributions, + n_samples, + seed=55, + xlabel=None, + ylabel=None, + title=None, + distrib_colors=None, + colorblind_safe=False, + show_plot=False): """ Plot samples from the given distribution. If several distributions should be plotted together, an array can be passed to this function. @@ -23,6 +32,11 @@ def plot_samples(distributions, n_samples, seed=55, xlabel=None, ylabel=None, ti label for y-axis. title : string, optional title for the plot. + distrib_colors : list or None, optional + List of colors to use for each distribution. If None, Matplotlib Set2 and glasbey colors will be used. + colorblind_safe : bool, optional + If True, the plot will use colors suitable for colorblind individuals. + Default is False. show_plot : bool, optional If True, display the plot. Default is False. @@ -37,9 +51,25 @@ def plot_samples(distributions, n_samples, seed=55, xlabel=None, ylabel=None, ti if isinstance(distributions, Distribution): distributions = [distributions] - for d in distributions: + + # Generate colors + if distrib_colors is None: + if colorblind_safe: + palette = gb.create_palette(palette_size=len(distributions), colorblind_safe=colorblind_safe) + else: + palette = utils.get_colors(len(distributions)) + else: + if len(distrib_colors) < len(distributions): + if colorblind_safe: + additional_colors = gb.create_palette(palette_size=len(distributions) - len(distrib_colors), colorblind_safe=colorblind_safe) + else: + additional_colors = utils.get_colors(len(distributions) - len(distrib_colors)) + distrib_colors.extend(additional_colors) + palette = distrib_colors + + for i, d in enumerate(distributions): samples = d.sample(n_samples, seed) - plt.scatter(x=samples[:,0], y=samples[:,1]) + plt.scatter(x=samples[:,0], y=samples[:,1], color=palette[i]) if xlabel: plt.xlabel(xlabel) if ylabel: @@ -57,7 +87,14 @@ def plot_samples(distributions, n_samples, seed=55, xlabel=None, ylabel=None, ti return fig, axs -def plot_contour(distributions, resolution=128, ranges=None, quantiles:list=None, seed=55, show_plot=False): +def plot_contour(distributions, + resolution=128, + ranges=None, + quantiles:list=None, + seed=55, + distrib_colors=None, + colorblind_safe=False, + show_plot=False): """ Plot contour plots for samples drawn from given distributions. @@ -73,6 +110,11 @@ def plot_contour(distributions, resolution=128, ranges=None, quantiles:list=None List of quantiles to use for determining isovalues. If None, the 99.7%, 95%, and 68% quantiles are used. seed : int Seed for the random number generator for reproducibility. It defaults to 55 if not provided. + distrib_colors : list or None, optional + List of colors to use for each distribution. If None, Matplotlib Set2 and glasbey colors will be used. + colorblind_safe : bool, optional + If True, the plot will use colors suitable for colorblind individuals. + Default is False. show_plot : bool, optional If True, display the plot. Default is False. @@ -92,7 +134,21 @@ def plot_contour(distributions, resolution=128, ranges=None, quantiles:list=None if isinstance(distributions, Distribution): distributions = [distributions] - contour_colors = generate_spectrum_colors(len(distributions)) + + # Generate colors + if distrib_colors is None: + if colorblind_safe: + palette = gb.create_palette(palette_size=len(distributions), colorblind_safe=colorblind_safe) + else: + palette = utils.get_colors(len(distributions)) + else: + if len(distrib_colors) < len(distributions): + if colorblind_safe: + additional_colors = gb.create_palette(palette_size=len(distributions) - len(distrib_colors), colorblind_safe=colorblind_safe) + else: + additional_colors = utils.get_colors(len(distributions) - len(distrib_colors)) + distrib_colors.extend(additional_colors) + palette = distrib_colors if ranges is None: min_val = np.zeros(distributions[0].mean().shape)+1000 @@ -114,7 +170,7 @@ def plot_contour(distributions, resolution=128, ranges=None, quantiles:list=None coordinates = coordinates.reshape((-1, 2)) pdf = d.pdf(coordinates) pdf = pdf.reshape(xv.shape) - color = contour_colors[i] + color = palette[i] # Monte Carlo approach for determining isovalues isovalues = [] @@ -147,7 +203,12 @@ def plot_contour(distributions, resolution=128, ranges=None, quantiles:list=None return fig, axs -def plot_contour_bands(distributions, n_samples, resolution=128, ranges=None, quantiles: list = None, seed=55, +def plot_contour_bands(distributions, + n_samples, + resolution=128, + ranges=None, + quantiles: list = None, + seed=55, show_plot=False): """ Plot contour bands for samples drawn from given distributions. @@ -186,13 +247,9 @@ def plot_contour_bands(distributions, n_samples, resolution=128, ranges=None, qu if isinstance(distributions, Distribution): distributions = [distributions] - # Sequential and perceptually uniform colormaps - colormaps = [ - 'Reds', 'Blues', 'Greens', 'Greys', 'Oranges', 'Purples', - 'YlOrBr', 'YlOrRd', 'OrRd', 'PuRd', 'RdPu', 'BuPu', - 'GnBu', 'PuBu', 'YlGnBu', 'PuBuGn', 'BuGn', 'YlGn', - 'viridis', 'plasma', 'inferno', 'magma', 'cividis' - ] + n_quantiles = len(quantiles) + alpha_values = np.linspace(1/n_quantiles, 1.0, n_quantiles) # Creates alpha values from 1/n to 1.0 + custom_cmap = utils.create_shaded_set2_colormap(alpha_values) if ranges is None: min_val = np.zeros(distributions[0].mean().shape)+1000 @@ -233,9 +290,18 @@ def plot_contour_bands(distributions, n_samples, resolution=128, ranges=None, qu elif int((1 - quantile/100) * n_samples) >= n_samples: raise ValueError(f"Quantile {quantile} results in an index that is out of bounds.") isovalues.append(densities[int((1 - quantile/100) * n_samples)]) + isovalues.append(densities[-1]) # Minimum density value + + # Extract the subset of colors corresponding to the current Set2 color and its 3 alpha variations + start_idx = i * n_quantiles + end_idx = start_idx + n_quantiles + color_subset = custom_cmap.colors[start_idx:end_idx] - # Generate logarithmic levels and create the contour plot with different colormap for each distribution - plt.contourf(xv, yv, pdf, levels=isovalues, locator=ticker.LogLocator(), cmap=colormaps[i % len(colormaps)]) + # Create a ListedColormap for the current color and its alpha variations + cmap_subset = ListedColormap(color_subset) + + # Generate the filled contour plot with transparency and better visibility + plt.contourf(xv, yv, pdf, levels=isovalues, cmap=cmap_subset) # Get the current figure and axes fig = plt.gcf() @@ -246,11 +312,3 @@ def plot_contour_bands(distributions, n_samples, resolution=128, ranges=None, qu plt.show() return fig, axs - -# HELPER FUNCTIONS -def generate_random_colors(length): - return ["#"+''.join([np.random.choice('0123456789ABCDEF') for j in range(6)]) for _ in range(length)] - -def generate_spectrum_colors(length): - cmap = plt.cm.get_cmap('viridis', length) # You can choose different colormaps like 'jet', 'hsv', 'rainbow', etc. - return np.array([cmap(i) for i in range(length)]) \ No newline at end of file diff --git a/uadapy/plotting/plotsND.py b/uadapy/plotting/plotsND.py index 04497d6..408b9b5 100644 --- a/uadapy/plotting/plotsND.py +++ b/uadapy/plotting/plotsND.py @@ -2,8 +2,14 @@ import numpy as np from uadapy import Distribution import uadapy.plotting.utils as utils - -def plot_samples(distributions, n_samples, seed=55, show_plot=False): +import glasbey as gb + +def plot_samples(distributions, + n_samples, + seed=55, + distrib_colors=None, + colorblind_safe=False, + show_plot=False): """ Plot samples from the multivariate distribution as a SLOM. @@ -15,6 +21,11 @@ def plot_samples(distributions, n_samples, seed=55, show_plot=False): Number of samples per distribution. seed : int Seed for the random number generator for reproducibility. It defaults to 55 if not provided. + distrib_colors : list or None, optional + List of colors to use for each distribution. If None, Matplotlib Set2 and glasbey colors will be used. + colorblind_safe : bool, optional + If True, the plot will use colors suitable for colorblind individuals. + Default is False. show_plot : bool, optional If True, display the plot. Default is False. @@ -32,7 +43,22 @@ def plot_samples(distributions, n_samples, seed=55, show_plot=False): # Create matrix n_dims = distributions[0].n_dims fig, axes = plt.subplots(nrows=n_dims, ncols=n_dims) - contour_colors = utils.generate_spectrum_colors(len(distributions)) + + # Generate colors + if distrib_colors is None: + if colorblind_safe: + palette = gb.create_palette(palette_size=len(distributions), colorblind_safe=colorblind_safe) + else: + palette = utils.get_colors(len(distributions)) + else: + if len(distrib_colors) < len(distributions): + if colorblind_safe: + additional_colors = gb.create_palette(palette_size=len(distributions) - len(distrib_colors), colorblind_safe=colorblind_safe) + else: + additional_colors = utils.get_colors(len(distributions) - len(distrib_colors)) + distrib_colors.extend(additional_colors) + palette = distrib_colors + for ax in axes.flat: # Hide all ticks and labels ax.xaxis.set_visible(False) @@ -45,11 +71,11 @@ def plot_samples(distributions, n_samples, seed=55, show_plot=False): samples = d.sample(n_samples, seed) for i, j in zip(*np.triu_indices_from(axes, k=1)): for x, y in [(i, j), (j, i)]: - axes[x,y].scatter(samples[:,y], y=samples[:,x], color=contour_colors[k]) + axes[x,y].scatter(samples[:,y], y=samples[:,x], color=palette[k]) # Fill diagonal for i in range(n_dims): - axes[i,i].hist(samples[:,i], histtype='stepfilled', fill=False, alpha=1.0, density=True, ec=contour_colors[k]) + axes[i,i].hist(samples[:,i], histtype='stepfilled', fill=False, alpha=1.0, density=True, ec=palette[k]) axes[i,i].xaxis.set_visible(True) axes[i,i].yaxis.set_visible(True) @@ -68,7 +94,15 @@ def plot_samples(distributions, n_samples, seed=55, show_plot=False): return fig, axs -def plot_contour(distributions, n_samples, resolution=128, ranges=None, quantiles: list = None, seed=55, show_plot=False): +def plot_contour(distributions, + n_samples, + resolution=128, + ranges=None, + quantiles: list = None, + seed=55, + distrib_colors=None, + colorblind_safe=False, + show_plot=False): """ Visualizes a multidimensional distribution in a matrix of contour plots. @@ -86,6 +120,11 @@ def plot_contour(distributions, n_samples, resolution=128, ranges=None, quantile List of quantiles to use for determining isovalues. If None, the 99.7%, 95%, and 68% quantiles are used. seed : int Seed for the random number generator for reproducibility. It defaults to 55 if not provided. + distrib_colors : list or None, optional + List of colors to use for each distribution. If None, Matplotlib Set2 and glasbey colors will be used. + colorblind_safe : bool, optional + If True, the plot will use colors suitable for colorblind individuals. + Default is False. show_plot : bool, optional If True, display the plot. Default is False. @@ -107,7 +146,22 @@ def plot_contour(distributions, n_samples, resolution=128, ranges=None, quantile if isinstance(distributions, Distribution): distributions = [distributions] - contour_colors = utils.generate_spectrum_colors(len(distributions)) + + # Generate colors + if distrib_colors is None: + if colorblind_safe: + palette = gb.create_palette(palette_size=len(distributions), colorblind_safe=colorblind_safe) + else: + palette = utils.get_colors(len(distributions)) + else: + if len(distrib_colors) < len(distributions): + if colorblind_safe: + additional_colors = gb.create_palette(palette_size=len(distributions) - len(distrib_colors), colorblind_safe=colorblind_safe) + else: + additional_colors = utils.get_colors(len(distributions) - len(distrib_colors)) + distrib_colors.extend(additional_colors) + palette = distrib_colors + # Create matrix n_dims = distributions[0].n_dims if ranges is None: @@ -161,7 +215,7 @@ def plot_contour(distributions, n_samples, resolution=128, ranges=None, quantile for i, j in zip(*np.triu_indices_from(axes, k=1)): for x, y in [(i, j), (j, i)]: - color = contour_colors[k] + color = palette[k] indices = list(np.arange(d.n_dims)) indices.remove(x) indices.remove(y) @@ -193,7 +247,14 @@ def plot_contour(distributions, n_samples, resolution=128, ranges=None, quantile return fig, axs -def plot_contour_samples(distributions, n_samples, resolution=128, ranges=None, quantiles: list = None, seed=55, +def plot_contour_samples(distributions, + n_samples, + resolution=128, + ranges=None, + quantiles: list = None, + seed=55, + distrib_colors=None, + colorblind_safe=False, show_plot=False): """ Visualizes a multidimensional distribution in a matrix visualization where the @@ -213,6 +274,11 @@ def plot_contour_samples(distributions, n_samples, resolution=128, ranges=None, List of quantiles to use for determining isovalues. If None, the 99.7%, 95%, and 68% quantiles are used. seed : int Seed for the random number generator for reproducibility. It defaults to 55 if not provided. + distrib_colors : list or None, optional + List of colors to use for each distribution. If None, Matplotlib Set2 and glasbey colors will be used. + colorblind_safe : bool, optional + If True, the plot will use colors suitable for colorblind individuals. + Default is False. show_plot : bool, optional If True, display the plot. Default is False. @@ -234,7 +300,22 @@ def plot_contour_samples(distributions, n_samples, resolution=128, ranges=None, if isinstance(distributions, Distribution): distributions = [distributions] - contour_colors = utils.generate_spectrum_colors(len(distributions)) + + # Generate colors + if distrib_colors is None: + if colorblind_safe: + palette = gb.create_palette(palette_size=len(distributions), colorblind_safe=colorblind_safe) + else: + palette = utils.get_colors(len(distributions)) + else: + if len(distrib_colors) < len(distributions): + if colorblind_safe: + additional_colors = gb.create_palette(palette_size=len(distributions) - len(distrib_colors), colorblind_safe=colorblind_safe) + else: + additional_colors = utils.get_colors(len(distributions) - len(distrib_colors)) + distrib_colors.extend(additional_colors) + palette = distrib_colors + # Create matrix n_dims = distributions[0].n_dims if ranges is None: @@ -287,7 +368,7 @@ def plot_contour_samples(distributions, n_samples, resolution=128, ranges=None, for i, j in zip(*np.triu_indices_from(axes, k=1)): for x, y in [(i, j), (j, i)]: - color = contour_colors[k] + color = palette[k] indices = list(np.arange(d.n_dims)) indices.remove(x) indices.remove(y) @@ -295,7 +376,7 @@ def plot_contour_samples(distributions, n_samples, resolution=128, ranges=None, if x < y: axes[x,y].contour(dims[x], dims[y], pdf_agg, levels=isovalues, colors=[color]) else: - axes[x, y].scatter(samples[:, y], y=samples[:, x], color=contour_colors[k]) + axes[x, y].scatter(samples[:, y], y=samples[:, x], color=palette[k]) axes[x, y].set_xlim(ranges[x][0], ranges[x][1]) axes[x, y].set_ylim(ranges[y][0], ranges[y][1]) diff --git a/uadapy/plotting/utils.py b/uadapy/plotting/utils.py index 67e9ae7..92180b8 100644 --- a/uadapy/plotting/utils.py +++ b/uadapy/plotting/utils.py @@ -1,9 +1,60 @@ import numpy as np import matplotlib.pyplot as plt +import glasbey as gb +from matplotlib.colors import ListedColormap def generate_random_colors(n): return ["#" +''.join([np.random.choice('0123456789ABCDEF') for j in range(6)]) for _ in range(n)] def generate_spectrum_colors(n): cmap = plt.cm.get_cmap('viridis', n) # You can choose different colormaps like 'jet', 'hsv', 'rainbow', etc. - return np.array([cmap(i) for i in range(n)]) \ No newline at end of file + return np.array([cmap(i) for i in range(n)]) + +def get_colors(n): + # Set2 colormap from matplotlib + set2_cmap = plt.get_cmap('Set2') + set2_colors = set2_cmap.colors + + # List to hold the final colors + final_colors = [] + + # First use the Set2 colormap + for i in range(min(n, len(set2_colors))): + final_colors.append(set2_colors[i]) + + # If more colors are needed, extend using Glasbey's generated palette + if n > len(set2_colors): + # Generate the additional colors using glasbey + glasbey_colors = gb.create_palette(palette_size=n - len(set2_colors)) + + # Extend final colors with glasbey's colors + final_colors.extend(glasbey_colors) + + return final_colors + +def create_shaded_set2_colormap(alpha_values): + """ + Create a custom colormap by varying alpha values for each Set2 color. + + Parameters: + - alpha_values: list or array of alpha values to apply to each Set2 color (e.g., [0.3, 0.6, 1]). + + Returns: + - A custom colormap with 8 Set2 colors, each with 3 alpha variations (total of 24 colors). + """ + # Get the 8 Set2 colors from matplotlib + set2_colors = plt.get_cmap('Set2').colors + + # Initialize a list to hold the new colors with alpha variations + shaded_colors = [] + + # Loop over each Set2 color and create alpha variations + for color in set2_colors: + for alpha in alpha_values: + rgba_color = list(color) + [alpha] # Add the alpha value to the RGB color + shaded_colors.append(rgba_color) # Append the RGBA color to the list + + # Create a ListedColormap with the shaded colors + custom_cmap = ListedColormap(shaded_colors) + + return custom_cmap