Skip to content

Commit adde6bf

Browse files
committed
Fit lifecycle charts to content and support transparent exports
1 parent d321ecd commit adde6bf

5 files changed

Lines changed: 71 additions & 6 deletions

File tree

‎docs/source/releases.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ These changes are prepared on the `github-actions` branch. PyGAD 3.8.0 has not b
2727
11. [Scramble mutation](utils.md#scramble_mutation) shuffles the selected segment's values directly, removing the separate index shuffle and reversal. Every permutation of that segment is possible; its values, array dtype, and unselected genes are preserved. Seeded results can differ from earlier versions. See issue [#76](https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/76).
2828
12. New examples explain [replacing a loaded fitness function](pygad.md#updating-the-fitness-function-after-loading), starting fresh when the objective changes, and [handling short final fitness batches](fitness_calculation.md#why-a-fitness-batch-can-be-smaller). The lifecycle guide also explains [progress reporting](lifecycle.md#reporting-progress) and the order of fitness evaluation and callbacks. See issues [#263](https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/263), [#217](https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/217), and [#154](https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/154).
2929
13. [Rank selection](utils.md#rank_selection) assigns descending selection weights to the best-to-worst sorted solutions, correcting a bias that gave worse solutions higher selection probabilities. Regression tests verify exact probabilities, original population indices, negative fitness, objective vectors, crowding distance, ties, and parent copies. See issue [#120](https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/120). Seeded rank-selection results can differ from earlier versions.
30-
14. A new [plot_lifecycle()](visualize.md#plot_lifecycle) method draws the lifecycle configured for a GA instance, including operators, callbacks, population replacement, generation loops, and stopping decisions. Stage annotations and a configuration panel show relevant settings, including gene types, batching, and offspring shapes. Use `show_parameters=False` for a compact view, `save_dir` to export SVG, PNG, or PDF, and `show=False` to create a chart without displaying it. The method works before or after `run()` without executing user functions or changing GA state. A new example is available at `examples/plots/example_plot_lifecycle.py`. The `pygad.visualize` submodule version is `1.2.1`.
30+
14. A new [plot_lifecycle()](visualize.md#plot_lifecycle) method draws the lifecycle configured for a GA instance, including operators, callbacks, population replacement, generation loops, and stopping decisions. Stage annotations and a configuration panel show relevant settings, including gene types, batching, and offspring shapes. Use `show_parameters=False` for a compact view, `save_dir` to export SVG, PNG, or PDF, and `show=False` to create a chart without displaying it. Use `transparent=True` for a transparent background. Charts fit their labels and connectors with small outer margins. The method works before or after `run()` without executing user functions or changing GA state. A new example is available at `examples/plots/example_plot_lifecycle.py`. The `pygad.visualize` submodule version is `1.2.1`.
3131

3232
15. [Duplicate-gene repair](gene_values.md#prevent-duplicates-in-gene-values) now uses one shared implementation for generated and manual initial populations, crossover, mutation, and NSGA-III population growth. Custom crossover and mutation outputs and their callbacks are also repaired when `allow_duplicate_genes=False`. Finite domains are searched completely through replacement chains, including changes to earlier duplicate occurrences. Continuous candidates and additional searches for dependent constraints use `sample_size`.
3333
16. Repair uses each destination gene's type, precision, and [range](gene_values.md#more-about-the-gene_space-parameter), and validates [constraints](gene_values.md#gene-constraint) against complete candidate solutions. Mixed types are compared by their exact stored numeric values. Mixed types, `sample_size=1`, stepped spaces, per-gene ranges, and `None` entries are handled consistently. Impossible initialization spaces warn instead of accessing uninitialized attributes. Equal and reversed integer bounds are handled consistently. Swap fallback uses original continuous and `None` bounds instead of membership in cached samples. SBX and polynomial mutation convert and round generated values before repair and use their own bounds. The `pygad.helper` and `pygad.utils` submodule versions are `1.4.2` and `1.5.4`.

‎docs/source/visualize.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,12 @@ Callbacks appear only when supplied. If `crossover_type=None` or `mutation_type=
4444

4545
In the detailed view, the `Stop Early?` block lists the configured `stop_criteria` and, when an `on_generation` callback is supplied, the possible condition `on_generation() returns "stop"`. Any one of these conditions ends the run. The chart does not analyze the callback's code or assume that it will return `"stop"`. The block is omitted when neither early stopping mechanism is configured. Built-in block and configuration titles capitalize the first letter of each word; method and handler names retain their original spelling.
4646

47-
Parameters: `title` (default `"PyGAD - Lifecycle"`), `font_size` (default `11`, finite and positive), `show_parameters` (default `True`), `save_dir` (default `None`), `show` (default `True`).
47+
Parameters: `title` (default `"PyGAD - Lifecycle"`), `font_size` (default `11`, finite and positive), `show_parameters` (default `True`), `save_dir` (default `None`), `show` (default `True`), `transparent` (default `False`).
4848

4949
Use `show_parameters=False` for a compact chart that keeps handler names and control flow. Set `show=False` to create or save a chart without displaying it. The method always returns the figure, so it can be customized further.
5050

51+
Charts fit their labels, cards, and connectors with small outer margins. Set `transparent=True` to remove the background for embedding in slides, videos, or web pages; the stage cards keep their fill colors.
52+
5153
```python
5254
# Save a detailed chart. The filename extension selects SVG, PNG, or PDF.
5355
fig = ga_instance.plot_lifecycle(title="PyGAD - Scheduling Optimization",

‎pygad/visualize/lifecycle.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,4 +400,19 @@ def wrap_text(text, width_inches, text_font_size, weight="normal"):
400400
linespacing=1.2, parse_math=False)
401401
configuration_top += 0.20 * len(label_lines) + 0.17 * len(value_lines) + 0.17
402402

403+
# Axes with axis("off") still occupy their full canvas in a tight export.
404+
# Fit the canvas to the actual labels, cards, and connectors instead.
405+
from matplotlib.transforms import Bbox
406+
fig.canvas.draw()
407+
renderer = fig.canvas.get_renderer()
408+
bounds = Bbox.union([artist.get_window_extent(renderer)
409+
for artist in list(axes.texts) + list(axes.patches)])
410+
data_bounds = bounds.transformed(axes.transData.inverted())
411+
margin = 0.08
412+
left, right = data_bounds.xmin - margin, data_bounds.xmax + margin
413+
top, bottom = data_bounds.ymin - margin, data_bounds.ymax + margin
414+
axes.set_xlim(left, right)
415+
axes.set_ylim(bottom, top)
416+
fig.set_size_inches((right - left) * figure_scale,
417+
(bottom - top) * figure_scale)
403418
return fig

‎pygad/visualize/plot.py‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ def plot_lifecycle(self,
3131
font_size=11,
3232
show_parameters=True,
3333
save_dir=None,
34-
show=True):
34+
show=True,
35+
transparent=False):
3536
"""
3637
Draw the configured lifecycle, including active operators,
3738
callbacks, the generation loop, and stopping conditions.
@@ -58,6 +59,9 @@ def plot_lifecycle(self,
5859
If True, display the figure. Set to False when saving
5960
charts in scripts, notebooks, or reports without showing
6061
a window. The figure is returned in either case.
62+
transparent : bool
63+
If True, use a transparent figure background when displaying
64+
or exporting the chart. Stage cards retain their fill colors.
6165
6266
Returns
6367
-------
@@ -79,8 +83,8 @@ def plot_lifecycle(self,
7983
raise TypeError("The font_size parameter must be a positive number.")
8084
if not numpy.isfinite(font_size) or font_size <= 0:
8185
raise ValueError("The font_size parameter must be finite and greater than 0.")
82-
if not isinstance(show_parameters, bool) or not isinstance(show, bool):
83-
raise TypeError("The show_parameters and show parameters must be bool values.")
86+
if not all(isinstance(value, bool) for value in (show_parameters, show, transparent)):
87+
raise TypeError("The show_parameters, show, and transparent parameters must be bool values.")
8488

8589
# Keep chart construction separate from rendering so its flow
8690
# can be checked without importing matplotlib or running a GA.
@@ -93,8 +97,13 @@ def plot_lifecycle(self,
9397
"pip install pygad[visualize] (or pip install matplotlib).") from exc
9498

9599
fig = _draw_lifecycle(lifecycle, matplt, title, font_size)
100+
if transparent:
101+
fig.patch.set_alpha(0)
102+
for axes in fig.axes:
103+
axes.patch.set_alpha(0)
96104
if save_dir is not None:
97-
fig.savefig(fname=save_dir, bbox_inches="tight")
105+
fig.savefig(fname=save_dir, bbox_inches="tight", pad_inches=0.02,
106+
transparent=transparent)
98107
if show:
99108
matplt.show()
100109
return fig

‎tests/test_plot_lifecycle.py‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,44 @@ def test_lifecycle_export_and_display_control(tmp_path, monkeypatch, extension):
397397
matplt.close(fig)
398398

399399

400+
@pytest.mark.parametrize("show_parameters", [True, False])
401+
def test_lifecycle_transparent_export_has_small_outer_margins(tmp_path, show_parameters):
402+
from PIL import Image
403+
404+
path = tmp_path / "transparent.png"
405+
fig = create_ga_instance().plot_lifecycle(show_parameters=show_parameters,
406+
transparent=True, save_dir=path, show=False)
407+
try:
408+
assert fig.patch.get_alpha() == 0
409+
with Image.open(path) as image:
410+
alpha = image.convert("RGBA").getchannel("A")
411+
left, top, right, bottom = alpha.getbbox()
412+
assert alpha.getextrema() == (0, 255)
413+
# Default exports use 100 dpi; a content crop leaves about ten
414+
# pixels of safety padding, rather than an unused axes canvas.
415+
assert max(left, top, image.width-right, image.height-bottom) <= 15
416+
fig.canvas.draw()
417+
renderer = fig.canvas.get_renderer()
418+
for text in fig.axes[0].texts:
419+
bounds = text.get_window_extent(renderer)
420+
assert fig.bbox.contains(*bounds.get_points()[0]), text.get_text()
421+
assert fig.bbox.contains(*bounds.get_points()[1]), text.get_text()
422+
finally:
423+
matplt.close(fig)
424+
425+
426+
def test_lifecycle_default_export_keeps_opaque_background(tmp_path):
427+
from PIL import Image
428+
429+
path = tmp_path / "opaque.png"
430+
fig = create_ga_instance().plot_lifecycle(save_dir=path, show=False)
431+
try:
432+
with Image.open(path) as image:
433+
assert image.convert("RGBA").getchannel("A").getextrema() == (255, 255)
434+
finally:
435+
matplt.close(fig)
436+
437+
400438
@pytest.mark.parametrize("parameters,error", [
401439
({"title": None}, TypeError),
402440
({"font_size": "large"}, TypeError),
@@ -407,6 +445,7 @@ def test_lifecycle_export_and_display_control(tmp_path, monkeypatch, extension):
407445
({"font_size": numpy.inf}, ValueError),
408446
({"show_parameters": "yes"}, TypeError),
409447
({"show": None}, TypeError),
448+
({"transparent": "yes"}, TypeError),
410449
])
411450
def test_lifecycle_parameter_validation(parameters, error):
412451
with pytest.raises(error):

0 commit comments

Comments
 (0)