Source code for app.components.figures.color_tools

"""
Color utilities for Plotly/Dash figures.

Includes helpers to translate color formats, darken colors, trim overly
light template colors, and assign consistent group/sample colors.
"""
import numpy as np
from matplotlib import pyplot as plt
import plotly.io as pio
import plotly.colors as pc

[docs] def rgba_to_hex(rgba: str) -> str: """Convert an RGBA color string to hex. :param rgba: Input as ``"rgba(r,g,b,a)"``. :returns: Hex color string, e.g. ``"#RRGGBB"``. """ return f'#{rgba.split("(")[1].split(")")[0].split(",")[0:3]}'
[docs] def remove_unwanted_colors(figure_template: str) -> str: """Return a Plotly template with overly light colors darkened. The function reads the colorway from the named template and adjusts entries with high luminance to improve visibility on light backgrounds. :param figure_template: Name of a Plotly template (e.g., ``"plotly"``). :returns: Modified template object with updated ``layout.colorway``. """ # grab the current template tmpl = pio.templates[figure_template] # take its colorway or fallback to default colorway = list(tmpl.layout.colorway or pio.templates["plotly"].layout.colorway) def too_light(rgb): r, g, b = pc.hex_to_rgb(rgb) # compute perceived brightness 0–255 luminance = 0.299*r + 0.587*g + 0.114*b return luminance > 210 # tweak threshold (230 ≈ very pale) # replace any light color with a darker variant safe_colors = [ rgb if not too_light(rgb) else pc.label_rgb(tuple(max(0, c-60) for c in pc.hex_to_rgb(rgb))) for rgb in colorway ] tmpl.layout.colorway = safe_colors return tmpl
[docs] def get_assigned_colors(sample_group_dict: dict) -> dict: """Assign per-group and per-sample colors. :param sample_group_dict: Mapping of group name to list of sample names. :returns: Tuple ``(colors, colors_with_contaminant)`` where both are dicts with keys ``'samples'`` and ``'sample groups'``. """ entry_list: list = list(sample_group_dict.keys()) colors: list = get_cut_colors(number_of_colors=len(entry_list)) group_colors: dict = {} for i, entry in enumerate(entry_list): group_colors[entry] = f'rgba({",".join(str(int(255*x)) for x in colors[i][:3])}, 1)' ret: dict = {'samples': {}, 'sample groups': group_colors} ret_cont: dict = {} for c in 'contaminant','non-contaminant': ret_cont[c] = {'samples': {}, 'sample groups': {}} for i, (sample_group, sample_list) in enumerate(sample_group_dict.items()): ret_cont['non-contaminant']['sample groups'][sample_group] = group_colors[sample_group] ret_cont['contaminant']['sample groups'][sample_group] = darken(group_colors[sample_group],20) for sample_name in sample_list: ret['samples'][sample_name] = group_colors[sample_group] ret_cont['non-contaminant']['samples'][sample_name] = group_colors[sample_group] ret_cont['contaminant']['samples'][sample_name] = darken(group_colors[sample_group],20) return (ret, ret_cont)
[docs] def darken(color: str, percent: int) -> str: """Darken a given RGB(A) color by a percentage. :param color: Color as ``"rgb(r,g,b)"`` or ``"rgba(r,g,b,a)"``. :param percent: Percentage to darken (0–100). :returns: Darkened color string of the same type. """ tp: str col_ints:list tp, col_ints = color.split('(') col_ints = [int(x) for x in col_ints.split(')')[0].split(',')] multiplier: float = ((100-percent)/100) col_ints = [str(max(0,int(c*multiplier))) for c in col_ints] if len(col_ints) == 4: # Make sure alpha is not 0 (=invisible) col_ints[-1] = '1' return f'{tp}({",".join(col_ints)})'
[docs] def get_cut_colors(colormapname: str = 'gist_ncar', number_of_colors: int = 15, cut: float = 0.4) -> list: """Return a list of evenly spaced colors from a matplotlib colormap. :param colormapname: Matplotlib colormap name. :param number_of_colors: Number of colors to sample. :param cut: Brightness adjustment mixed with white (0–1). :returns: List of RGBA tuples in 0–1 range. """ number_of_colors += 1 colors: list = (1. - cut) * (plt.get_cmap(colormapname)(np.linspace(0., 1., number_of_colors))) + \ cut * np.ones((number_of_colors, 4)) colors = colors[:-1] return colors