Source code for chromo_map.core.swatch

# mypy: ignore-errors
"""Swatch class for chromo_map package."""

import uuid
from textwrap import dedent
from typing import TYPE_CHECKING, Any, Dict, Union

from IPython.display import HTML
from jinja2 import Template

if TYPE_CHECKING:
    from chromo_map.core.color import Color
    from chromo_map.core.gradient import Gradient
else:
    from chromo_map.core.color import Color
    from chromo_map.core.gradient import Gradient


[docs] class Swatch: """A class for representing a collection of gradients."""
[docs] def __init__(self, gradients, maxn=32): self.maxn = maxn self.gradients = [] if isinstance(gradients, dict): for name, colors in gradients.items(): try: self.gradients.append(Gradient(colors, name=name)) except ValueError as e: raise e elif isinstance(gradients, (list, tuple)): self.gradients = list(gradients) else: raise ValueError("Gradients must be a dict or list of Gradient objects")
[docs] def to_dict(self): """Convert to a dictionary of gradient names to colors.""" return {gradient.name: gradient.colors for gradient in self.gradients}
[docs] def __iter__(self): """Iterate over gradients.""" return iter(self.gradients)
[docs] def __len__(self): """Return the number of gradients.""" return len(self.gradients)
[docs] def with_max(self, maxn): """Return a new Swatch with a different maximum number of gradients.""" return Swatch(self.gradients, maxn=maxn)
[docs] def to_grid(self, as_png=False): """Convert the swatch to an HTML grid.""" n = len(self.gradients) if n == 0: return "" template = Template( dedent( """\ <div id="_{{ random_id }}" class="color-swatch"> <style> #_{{ random_id }} { display: grid; grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr)); gap: 0.5rem 1rem; justify-content: space-between; overflow: hidden; resize: both; width: min(65rem, 100%); } #_{{ random_id }} div { width: 100%; } #_{{ random_id }} > div.gradient { width: 100%; height: min(4rem, 100%); display: grid; gap: 0.2rem; grid-template-rows: 1rem auto; } #_{{ random_id }} .color { height: minmax(1.5rem, 100%); } #_{{ random_id }} > div.gradient > strong { margin: 0; padding: 0; } #_{{ random_id }} img {height: 100%;} </style> {% for gradient in gradients %} {{ gradient.to_div(maxn, as_png=as_png).data }} {% endfor %} </div> """ ) ) random_id = uuid.uuid4().hex return HTML( template.render( gradients=self.gradients, random_id=random_id, maxn=self.maxn, as_png=as_png, ) )
[docs] def append(self, gradient: "Gradient"): """Append a new gradient to the swatch. Parameters ---------- gradient : Gradient The gradient to append. Returns ------- None Examples -------- Append a new gradient to the swatch: .. testcode:: from chromo_map import Swatch, Gradient swatch = Swatch([]) swatch.append(Gradient(['#ff0000', '#00ff00', '#0000ff'], name='RGB')) .. html-output:: from chromo_map import Swatch, Gradient swatch = Swatch([]) swatch.append(Gradient(['#ff0000', '#00ff00', '#0000ff'], name='RGB')) print(swatch.to_grid().data) """ self.gradients.append(gradient)
[docs] def adjust_hue(self, degrees: float) -> "Swatch": """Adjust the hue of all gradients in the swatch. Parameters ---------- degrees : float The number of degrees to adjust the hue by. Returns ------- Swatch A new swatch with adjusted hue. Examples -------- Adjust hue by 90 degrees: .. testcode:: from chromo_map import Swatch, Gradient gradients = [Gradient(['#ff0000', '#00ff00'], name='RedGreen')] swatch = Swatch(gradients) shifted = swatch.adjust_hue(90) shifted .. html-output:: from chromo_map import Swatch, Gradient gradients = [Gradient(['#ff0000', '#00ff00'], name='RedGreen')] swatch = Swatch(gradients) shifted = swatch.adjust_hue(90) print(shifted._repr_html_()) """ new_gradients = [gradient.adjust_hue(degrees) for gradient in self.gradients] return Swatch(new_gradients, maxn=self.maxn)
[docs] def adjust_saturation(self, factor: float) -> "Swatch": """Adjust the saturation of all gradients in the swatch. Parameters ---------- factor : float The factor to multiply saturation by. Returns ------- Swatch A new swatch with adjusted saturation. Examples -------- Decrease saturation by 25%: .. testcode:: from chromo_map import Swatch, Gradient gradients = [Gradient(['#ff0000', '#00ff00'], name='RedGreen')] swatch = Swatch(gradients) desaturated = swatch.adjust_saturation(0.75) desaturated .. html-output:: from chromo_map import Swatch, Gradient gradients = [Gradient(['#ff0000', '#00ff00'], name='RedGreen')] swatch = Swatch(gradients) desaturated = swatch.adjust_saturation(0.75) print(desaturated._repr_html_()) """ new_gradients = [ gradient.adjust_saturation(factor) for gradient in self.gradients ] return Swatch(new_gradients, maxn=self.maxn)
[docs] def adjust_brightness(self, factor: float) -> "Swatch": """Adjust the brightness of all gradients in the swatch. Parameters ---------- factor : float The factor to multiply brightness by. Returns ------- Swatch A new swatch with adjusted brightness. Examples -------- Increase brightness by 10%: .. testcode:: from chromo_map import Swatch, Gradient gradients = [Gradient(['#ff0000', '#00ff00'], name='RedGreen')] swatch = Swatch(gradients) bright = swatch.adjust_brightness(1.1) bright .. html-output:: from chromo_map import Swatch, Gradient gradients = [Gradient(['#ff0000', '#00ff00'], name='RedGreen')] swatch = Swatch(gradients) bright = swatch.adjust_brightness(1.1) print(bright._repr_html_()) """ new_gradients = [ gradient.adjust_brightness(factor) for gradient in self.gradients ] return Swatch(new_gradients, maxn=self.maxn)
[docs] def adjust_lightness(self, factor: float) -> "Swatch": """Adjust the lightness of all gradients in the swatch. Parameters ---------- factor : float The factor to multiply lightness by. Returns ------- Swatch A new swatch with adjusted lightness. Examples -------- Decrease lightness by 15%: .. testcode:: from chromo_map import Swatch, Gradient gradients = [Gradient(['#ff0000', '#00ff00'], name='RedGreen')] swatch = Swatch(gradients) dark = swatch.adjust_lightness(0.85) dark .. html-output:: from chromo_map import Swatch, Gradient gradients = [Gradient(['#ff0000', '#00ff00'], name='RedGreen')] swatch = Swatch(gradients) dark = swatch.adjust_lightness(0.85) print(dark._repr_html_()) """ new_gradients = [ gradient.adjust_lightness(factor) for gradient in self.gradients ] return Swatch(new_gradients, maxn=self.maxn)
[docs] def make_accessible( self, background_color: Union[Color, str], level: str = "AA" ) -> "Swatch": """Make all gradients in the swatch accessible against a background color. Parameters ---------- background_color : Color or str The background color to ensure accessibility against. level : str, default 'AA' The WCAG level to achieve ('AA' or 'AAA'). Returns ------- Swatch A new swatch with accessible gradients. Examples -------- Make swatch accessible against white background: .. testcode:: from chromo_map import Swatch, Gradient gradients = [Gradient(['#ffcccc', '#ccffcc'], name='Pastels')] swatch = Swatch(gradients) accessible = swatch.make_accessible('white') accessible .. html-output:: from chromo_map import Swatch, Gradient gradients = [Gradient(['#ffcccc', '#ccffcc'], name='Pastels')] swatch = Swatch(gradients) accessible = swatch.make_accessible('white') print(accessible._repr_html_()) """ new_gradients = [ gradient.make_accessible(background_color, level) for gradient in self.gradients ] return Swatch(new_gradients, maxn=self.maxn)
[docs] def complementary(self) -> "Swatch": """Get the complementary swatch (all gradients with complementary colors). Returns ------- Swatch A new swatch with complementary gradients. Examples -------- Get complementary swatch: .. testcode:: from chromo_map import Swatch, Gradient gradients = [Gradient(['#ff0000', '#00ff00'], name='RedGreen')] swatch = Swatch(gradients) complement = swatch.complementary() complement .. html-output:: from chromo_map import Swatch, Gradient gradients = [Gradient(['#ff0000', '#00ff00'], name='RedGreen')] swatch = Swatch(gradients) complement = swatch.complementary() print(complement._repr_html_()) """ new_gradients = [gradient.complementary() for gradient in self.gradients] return Swatch(new_gradients, maxn=self.maxn)
[docs] def analyze_contrast(self, background_color: Union[Color, str]) -> Dict[str, Any]: """Analyze contrast ratios of all gradients against a background. Parameters ---------- background_color : Color or str The background color to analyze against. Returns ------- Dict[str, Any] Dictionary containing contrast analysis results for each gradient. Examples -------- Analyze contrast against white background: .. testcode:: from chromo_map import Swatch, Gradient gradients = [Gradient(['#000000', '#808080'], name='Darks')] swatch = Swatch(gradients) analysis = swatch.analyze_contrast('white') print(f"Gradients analyzed: {len(analysis['gradients'])}") .. testoutput:: Gradients analyzed: 1 """ background = ( Color(background_color) if not isinstance(background_color, Color) else background_color ) gradient_analyses = [] all_contrasts = [] for gradient in self.gradients: analysis = gradient.analyze_contrast(background) gradient_analyses.append({"name": gradient.name, "analysis": analysis}) all_contrasts.extend(analysis["contrasts"]) # Overall statistics total_colors = len(all_contrasts) accessible_aa = sum(1 for c in all_contrasts if c >= 4.5) accessible_aaa = sum(1 for c in all_contrasts if c >= 7.0) return { "gradients": gradient_analyses, "total_colors": total_colors, "overall_average_contrast": ( sum(all_contrasts) / total_colors if all_contrasts else 0 ), "overall_min_contrast": min(all_contrasts) if all_contrasts else 0, "overall_max_contrast": max(all_contrasts) if all_contrasts else 0, "overall_accessible_aa_count": accessible_aa, "overall_accessible_aaa_count": accessible_aaa, "overall_accessibility_aa_score": ( accessible_aa / total_colors if total_colors else 0 ), "overall_accessibility_aaa_score": ( accessible_aaa / total_colors if total_colors else 0 ), }
[docs] def find_accessible_version( self, background_color: Union[Color, str], level: str = "AA" ) -> "Swatch": """Find accessible version of all gradients in the swatch. Parameters ---------- background_color : Color or str The background color to ensure accessibility against. level : str, default 'AA' The WCAG level to achieve ('AA' or 'AAA'). Returns ------- Swatch A new swatch with accessible gradients. Examples -------- Find accessible version of swatch: .. testcode:: from chromo_map import Swatch, Gradient gradients = [Gradient(['#ffcccc', '#ccffcc'], name='Light')] swatch = Swatch(gradients) accessible = swatch.find_accessible_version('white') accessible .. html-output:: from chromo_map import Swatch, Gradient gradients = [Gradient(['#ffcccc', '#ccffcc'], name='Light')] swatch = Swatch(gradients) accessible = swatch.find_accessible_version('white') print(accessible._repr_html_()) """ new_gradients = [ gradient.find_accessible_version(background_color, level) for gradient in self.gradients ] return Swatch(new_gradients, maxn=self.maxn)
[docs] def maximize_contrast_iterative( self, background_color: Union[Color, str], level: str = "AA", adjust_lightness: bool = True, step_size: float = 0.1, max_attempts: int = 50, ) -> "Swatch": """Maximize contrast of all gradients using iterative approach. Parameters ---------- background_color : Color or str The background color to maximize contrast against. level : str, default 'AA' The WCAG level to achieve ('AA' or 'AAA'). adjust_lightness : bool, default True Whether to adjust lightness (True) or brightness/value (False). step_size : float, default 0.1 The step size for adjustments. max_attempts : int, default 50 Maximum number of adjustment attempts. Returns ------- Swatch A new swatch with maximized contrast gradients. Examples -------- Maximize contrast iteratively: .. testcode:: from chromo_map import Swatch, Gradient gradients = [Gradient(['#ff6666', '#66ff66'], name='Light')] swatch = Swatch(gradients) optimized = swatch.maximize_contrast_iterative('white') optimized .. html-output:: from chromo_map import Swatch, Gradient gradients = [Gradient(['#ff6666', '#66ff66'], name='Light')] swatch = Swatch(gradients) optimized = swatch.maximize_contrast_iterative('white') print(optimized._repr_html_()) """ new_gradients = [ gradient.maximize_contrast_iterative( background_color, level, adjust_lightness, step_size, max_attempts ) for gradient in self.gradients ] return Swatch(new_gradients, maxn=self.maxn)
[docs] def maximize_contrast_optimization( self, background_color: Union[Color, str], level: str = "AA", method: str = "golden_section", ) -> "Swatch": """Maximize contrast of all gradients using mathematical optimization. Parameters ---------- background_color : Color or str The background color to maximize contrast against. level : str, default 'AA' The WCAG level to achieve ('AA' or 'AAA'). method : str, default 'golden_section' The optimization method to use ('golden_section' or 'gradient_descent'). Returns ------- Swatch A new swatch with maximized contrast gradients. Examples -------- Maximize contrast with optimization: .. testcode:: from chromo_map import Swatch, Gradient gradients = [Gradient(['#ff6666', '#66ff66'], name='Light')] swatch = Swatch(gradients) optimized = swatch.maximize_contrast_optimization('white') optimized .. html-output:: from chromo_map import Swatch, Gradient gradients = [Gradient(['#ff6666', '#66ff66'], name='Light')] swatch = Swatch(gradients) optimized = swatch.maximize_contrast_optimization('white') print(optimized._repr_html_()) """ new_gradients = [ gradient.maximize_contrast_optimization(background_color, level, method) for gradient in self.gradients ] return Swatch(new_gradients, maxn=self.maxn)
def _repr_html_(self) -> str: """Return HTML representation for Jupyter notebook display.""" result = self.to_grid() if hasattr(result, "data"): return result.data return str(result)