-
Notifications
You must be signed in to change notification settings - Fork 9
Feature/component colors #440
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
5fc4dca
Add new config options for plotting
FBumann 3edcf40
Use turbo instead of viridis
FBumann 9ce213c
Update plotting.py to use updated color management
FBumann dfa385f
update color management
FBumann 2346759
Add rgb to hex for matplotlib
FBumann acdf93d
Add rgb to hex for matplotlib
FBumann 5c24d25
Remove colormanager class
FBumann e7b0a1e
Update type hints
FBumann cabe8be
Update type hints and use Config defaults
FBumann 94c16ba
Add stable colors
FBumann 3465005
V1
FBumann f2848fc
V2
FBumann 2bc0624
Use calculation.colors if direct colors is None
FBumann 472cf1c
Bugfix
FBumann 7f790e4
Bugfix
FBumann 72b2a2c
Update setup_colors
FBumann 3fcdbff
Add color setup to examples
FBumann 4740763
Final touches
FBumann 664e8ff
Update CHANGELOG.md
FBumann f6c721b
Update CHANGELOG.md
FBumann 59c399f
Bugfix
FBumann 0fd989b
Update fro SegmentedCalculationResults
FBumann 9a7b8d7
Default show = False in tests
FBumann c1622ff
Bugfix
FBumann bff1ad6
Bugfix
FBumann 4e64f52
Add show default to plot_network
FBumann 8d458b7
Make _rgb_string_to_hex more robust
FBumann 9145cce
Improve Error Handling
FBumann 8822cd6
Overwrite colors explicitly in setup_colors
FBumann e94a61c
Improve config loader
FBumann e697ac0
Update CHANGELOG.md
FBumann a36ce89
Make colors arg always overwrite the default behaviour
FBumann File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,261 @@ | ||
| """Simplified color handling for visualization. | ||
|
|
||
| This module provides clean color processing that transforms various input formats | ||
| into a label-to-color mapping dictionary, without needing to know about the plotting engine. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
|
|
||
| import matplotlib.colors as mcolors | ||
| import matplotlib.pyplot as plt | ||
| import plotly.express as px | ||
| from plotly.exceptions import PlotlyError | ||
|
|
||
| logger = logging.getLogger('flixopt') | ||
|
|
||
|
|
||
| def _rgb_string_to_hex(color: str) -> str: | ||
| """Convert Plotly RGB/RGBA string format to hex. | ||
|
|
||
| Args: | ||
| color: Color in format 'rgb(R, G, B)', 'rgba(R, G, B, A)' or already in hex | ||
|
|
||
| Returns: | ||
| Color in hex format '#RRGGBB' | ||
| """ | ||
| color = color.strip() | ||
|
|
||
| # If already hex, return as-is | ||
| if color.startswith('#'): | ||
| return color | ||
|
|
||
| # Try to parse rgb() or rgba() | ||
| try: | ||
| if color.startswith('rgb('): | ||
| # Extract RGB values from 'rgb(R, G, B)' format | ||
| rgb_str = color[4:-1] # Remove 'rgb(' and ')' | ||
| elif color.startswith('rgba('): | ||
| # Extract RGBA values from 'rgba(R, G, B, A)' format | ||
| rgb_str = color[5:-1] # Remove 'rgba(' and ')' | ||
| else: | ||
| return color | ||
|
|
||
| # Split on commas and parse first three components | ||
| components = rgb_str.split(',') | ||
| if len(components) < 3: | ||
| return color | ||
|
|
||
| # Parse and clamp the first three components | ||
| r = max(0, min(255, int(round(float(components[0].strip()))))) | ||
| g = max(0, min(255, int(round(float(components[1].strip()))))) | ||
| b = max(0, min(255, int(round(float(components[2].strip()))))) | ||
|
|
||
| return f'#{r:02x}{g:02x}{b:02x}' | ||
| except (ValueError, IndexError): | ||
| # If parsing fails, return original | ||
| return color | ||
|
|
||
|
|
||
| def process_colors( | ||
| colors: None | str | list[str] | dict[str, str], | ||
| labels: list[str], | ||
| default_colorscale: str = 'turbo', | ||
| ) -> dict[str, str]: | ||
| """Process color input and return a label-to-color mapping. | ||
|
|
||
| This function takes flexible color input and always returns a dictionary | ||
| mapping each label to a specific color string. The plotting engine can then | ||
| use this mapping as needed. | ||
|
|
||
| Args: | ||
| colors: Color specification in one of four formats: | ||
| - None: Use the default colorscale | ||
| - str: Name of a colorscale (e.g., 'turbo', 'plasma', 'Set1', 'portland') | ||
| - list[str]: List of color strings (hex, named colors, etc.) | ||
| - dict[str, str]: Direct label-to-color mapping | ||
| labels: List of labels that need colors assigned | ||
| default_colorscale: Fallback colorscale name if requested scale not found | ||
|
|
||
| Returns: | ||
| Dictionary mapping each label to a color string | ||
|
|
||
| Examples: | ||
| >>> # Using None - applies default colorscale | ||
| >>> process_colors(None, ['A', 'B', 'C']) | ||
| {'A': '#0d0887', 'B': '#7e03a8', 'C': '#cc4778'} | ||
|
|
||
| >>> # Using a colorscale name | ||
| >>> process_colors('plasma', ['A', 'B', 'C']) | ||
| {'A': '#0d0887', 'B': '#7e03a8', 'C': '#cc4778'} | ||
|
|
||
| >>> # Using a list of colors | ||
| >>> process_colors(['red', 'blue', 'green'], ['A', 'B', 'C']) | ||
| {'A': 'red', 'B': 'blue', 'C': 'green'} | ||
|
|
||
| >>> # Using a pre-made mapping | ||
| >>> process_colors({'A': 'red', 'B': 'blue'}, ['A', 'B', 'C']) | ||
| {'A': 'red', 'B': 'blue', 'C': '#0d0887'} # C gets color from default scale | ||
| """ | ||
| if not labels: | ||
| return {} | ||
|
|
||
| # Case 1: Already a mapping dictionary | ||
| if isinstance(colors, dict): | ||
| return _fill_missing_colors(colors, labels, default_colorscale) | ||
|
|
||
| # Case 2: None or colorscale name (string) | ||
| if colors is None or isinstance(colors, str): | ||
| colorscale_name = colors if colors is not None else default_colorscale | ||
| color_list = _get_colors_from_scale(colorscale_name, len(labels), default_colorscale) | ||
| return dict(zip(labels, color_list, strict=False)) | ||
|
|
||
| # Case 3: List of colors | ||
| if isinstance(colors, list): | ||
| if len(colors) == 0: | ||
| logger.warning(f'Empty color list provided. Using {default_colorscale} instead.') | ||
| color_list = _get_colors_from_scale(default_colorscale, len(labels), default_colorscale) | ||
| return dict(zip(labels, color_list, strict=False)) | ||
|
|
||
| if len(colors) < len(labels): | ||
| logger.debug( | ||
| f'Not enough colors provided ({len(colors)}) for all labels ({len(labels)}). Colors will cycle.' | ||
| ) | ||
|
|
||
| # Cycle through colors if we don't have enough | ||
| return {label: colors[i % len(colors)] for i, label in enumerate(labels)} | ||
|
|
||
| raise TypeError(f'colors must be None, str, list, or dict, got {type(colors)}') | ||
|
|
||
|
|
||
| def _fill_missing_colors( | ||
| color_mapping: dict[str, str], | ||
| labels: list[str], | ||
| default_colorscale: str, | ||
| ) -> dict[str, str]: | ||
| """Fill in missing labels in a color mapping using a colorscale. | ||
|
|
||
| Args: | ||
| color_mapping: Partial label-to-color mapping | ||
| labels: All labels that need colors | ||
| default_colorscale: Colorscale to use for missing labels | ||
|
|
||
| Returns: | ||
| Complete label-to-color mapping | ||
| """ | ||
| missing_labels = [label for label in labels if label not in color_mapping] | ||
|
|
||
| if not missing_labels: | ||
| return color_mapping.copy() | ||
|
|
||
| # Log warning about missing labels | ||
| logger.debug(f'Labels missing colors: {missing_labels}. Using {default_colorscale} for these.') | ||
|
|
||
| # Get colors for missing labels | ||
| missing_colors = _get_colors_from_scale(default_colorscale, len(missing_labels), default_colorscale) | ||
|
|
||
| # Combine existing and new colors | ||
| result = color_mapping.copy() | ||
| result.update(dict(zip(missing_labels, missing_colors, strict=False))) | ||
| return result | ||
|
|
||
|
|
||
| def _get_colors_from_scale( | ||
| colorscale_name: str, | ||
| num_colors: int, | ||
| fallback_scale: str, | ||
| ) -> list[str]: | ||
| """Extract a list of colors from a named colorscale. | ||
|
|
||
| Tries to get colors from the named scale (Plotly first, then Matplotlib), | ||
| falls back to the fallback scale if not found. | ||
|
|
||
| Args: | ||
| colorscale_name: Name of the colorscale to try | ||
| num_colors: Number of colors needed | ||
| fallback_scale: Fallback colorscale name if first fails | ||
|
|
||
| Returns: | ||
| List of color strings (hex format) | ||
| """ | ||
| # Try to get the requested colorscale | ||
| colors = _try_get_colorscale(colorscale_name, num_colors) | ||
|
|
||
| if colors is not None: | ||
| return colors | ||
|
|
||
| # Fallback to default | ||
| logger.warning(f"Colorscale '{colorscale_name}' not found. Using '{fallback_scale}' instead.") | ||
|
|
||
| colors = _try_get_colorscale(fallback_scale, num_colors) | ||
|
|
||
| if colors is not None: | ||
| return colors | ||
|
|
||
| # Ultimate fallback: just use basic colors | ||
| logger.warning(f"Fallback colorscale '{fallback_scale}' also not found. Using basic colors.") | ||
| basic_colors = [ | ||
| '#1f77b4', | ||
| '#ff7f0e', | ||
| '#2ca02c', | ||
| '#d62728', | ||
| '#9467bd', | ||
| '#8c564b', | ||
| '#e377c2', | ||
| '#7f7f7f', | ||
| '#bcbd22', | ||
| '#17becf', | ||
| ] | ||
| return [basic_colors[i % len(basic_colors)] for i in range(num_colors)] | ||
|
|
||
|
|
||
| def _try_get_colorscale(colorscale_name: str, num_colors: int) -> list[str] | None: | ||
| """Try to get colors from Plotly or Matplotlib colorscales. | ||
|
|
||
| Tries Plotly colorscales first (both qualitative and sequential), | ||
| then falls back to Matplotlib colorscales. | ||
|
|
||
| Args: | ||
| colorscale_name: Name of the colorscale | ||
| num_colors: Number of colors needed | ||
|
|
||
| Returns: | ||
| List of color strings (hex format) if successful, None if colorscale not found | ||
| """ | ||
| # First try Plotly qualitative (discrete) color sequences | ||
| colorscale_title = colorscale_name.title() | ||
| if hasattr(px.colors.qualitative, colorscale_title): | ||
| color_list = getattr(px.colors.qualitative, colorscale_title) | ||
| # Convert to hex format for matplotlib compatibility | ||
| return [_rgb_string_to_hex(color_list[i % len(color_list)]) for i in range(num_colors)] | ||
|
|
||
| # Then try Plotly sequential/continuous colorscales | ||
| try: | ||
| colorscale = px.colors.get_colorscale(colorscale_name) | ||
| # Sample evenly from the colorscale | ||
| if num_colors == 1: | ||
| sample_points = [0.5] | ||
| else: | ||
| sample_points = [i / (num_colors - 1) for i in range(num_colors)] | ||
| colors = px.colors.sample_colorscale(colorscale, sample_points) | ||
| # Convert to hex format for matplotlib compatibility | ||
| return [_rgb_string_to_hex(c) for c in colors] | ||
| except (PlotlyError, ValueError): | ||
| pass | ||
|
|
||
| # Finally try Matplotlib colorscales | ||
| try: | ||
| cmap = plt.get_cmap(colorscale_name) | ||
|
|
||
| # Sample evenly from the colorscale | ||
| if num_colors == 1: | ||
| colors = [cmap(0.5)] | ||
| else: | ||
| colors = [cmap(i / (num_colors - 1)) for i in range(num_colors)] | ||
|
|
||
| # Convert RGBA tuples to hex strings | ||
| return [mcolors.rgb2hex(color[:3]) for color in colors] | ||
|
|
||
| except (ValueError, KeyError): | ||
| return None | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.