class MonochromePaletteGenerator: def __init__(self, max_palette_lightness: int = 95): self.MIN_ALLOWED_LIGHTNESS = 15 self.max_palette_lightness = max_palette_lightness def __validate_starting_lightness(self, starting_lightness: int) -> bool: if starting_lightness <= self.MIN_ALLOWED_LIGHTNESS: return True return False @staticmethod def hex_to_hsl(hex_color: str) -> tuple: # Remove the "#" character if present hex_color = hex_color.lstrip('#') # Convert hex to RGB r = int(hex_color[0:2], 16) / 255.0 g = int(hex_color[2:4], 16) / 255.0 b = int(hex_color[4:6], 16) / 255.0 # Find the maximum and minimum RGB values max_val = max(r, g, b) min_val = min(r, g, b) delta = max_val - min_val # Calculate Lightness L = (max_val + min_val) / 2 # Calculate Saturation if delta == 0: S = 0 # It's a shade of gray, so no saturation H = 0 # Hue is undefined for gray, but we can set it to 0 else: if L < 0.5: S = delta / (max_val + min_val) else: S = delta / (2.0 - max_val - min_val) # Calculate Hue if max_val == r: H = ((g - b) / delta) % 6 elif max_val == g: H = (b - r) / delta + 2 elif max_val == b: H = (r - g) / delta + 4 H *= 60 # Convert hue to degrees if H < 0: H += 360 return int(round(H)), int(round(S * 100)), int(round(L * 100)) @staticmethod def hsl_to_hex(h: int, s: int, l: int) -> str: # Convert the saturation and lightness percentages to a fraction of 1 s /= 100 l /= 100 # Calculate C, X, and m C = (1 - abs(2 * l - 1)) * s # Chroma X = C * (1 - abs((h / 60) % 2 - 1)) # Intermediate value based on hue m = l - C / 2 # Lightness adjustment # Calculate r', g', b' (primed values) if 0 <= h < 60: r_prime, g_prime, b_prime = C, X, 0 elif 60 <= h < 120: r_prime, g_prime, b_prime = X, C, 0 elif 120 <= h < 180: r_prime, g_prime, b_prime = 0, C, X elif 180 <= h < 240: r_prime, g_prime, b_prime = 0, X, C elif 240 <= h < 300: r_prime, g_prime, b_prime = X, 0, C elif 300 <= h < 360: r_prime, g_prime, b_prime = C, 0, X else: raise ValueError("Hue value must be in range [0, 360)") # Convert r', g', b' to the range [0, 255] r = round((r_prime + m) * 255) g = round((g_prime + m) * 255) b = round((b_prime + m) * 255) # Convert RGB to hex return "#{:02X}{:02X}{:02X}".format(r, g, b) def __create_lightness_range(self, starting_lightness: int, n_colors: int) -> list: # Make `n_colors` number of linearly spaced lightness values between thr provided starting color # and max allowed lightness spaced_values = np.linspace(starting_lightness, self.max_palette_lightness, n_colors) lightness = np.round(spaced_values).astype(int) return lightness def create_hex_code_palette(self, starting_hex: str, n_colors: int) -> list: # Convert HEX to HSL starting_hsl = self.hex_to_hsl(hex_color=starting_hex) # Check if HSL is valid is_valid_starting_hex = self.__validate_starting_lightness(starting_lightness=starting_hsl[2]) # Proceed only if the starting HSL is valid if is_valid_starting_hex: palette = [] lightness_range = self.__create_lightness_range(starting_lightness=starting_hsl[2], n_colors=n_colors) for lightness in lightness_range: # Keep hue and saturation identical h = starting_hsl[0] s = starting_hsl[1] # Only change the lightness curr_hex = self.hsl_to_hex(h=h, s=s, l=lightness) palette.append(curr_hex) return palette raise ValueError("Given starting color is too light to construct a palette. Please choose a darker shade.") @staticmethod def create_matplotlib_palette(colors: list, palette_name: str) -> mpl.colors.ListedColormap: return mpl.colors.ListedColormap( name=palette_name, colors=colors )