diff --git a/README.md b/README.md
index 9368ed4..70b67da 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# con/duct Examples Gallery
> 🤖 Automatically generated gallery of con/duct usage examples
-> Last updated: 2026-04-25 03:19 UTC
+> Last updated: 2026-05-06 17:30 UTC
## 📚 Browse by Tag
@@ -43,7 +43,10 @@
Demo example from the con/duct repository showing resource usage tracking
-
+
+| ps pcpu (raw) | ps cpu (time-point estimate) |
+ |  |
+
📋 Metadata
@@ -62,7 +65,10 @@ Demo example from the con/duct repository showing resource usage tracking
**Tags**: [`synthetic`](#synthetic) [`asmacdo`](#asmacdo)
**Repository**: [github.com/asmacdo/asmacdo-duct-gallery](https://github.com/asmacdo/asmacdo-duct-gallery/)
-
+
+| ps pcpu (raw) | ps cpu (time-point estimate) |
+ |  |
+
📋 Metadata
@@ -81,7 +87,10 @@ Demo example from the con/duct repository showing resource usage tracking
**Tags**: [`synthetic`](#synthetic) [`asmacdo`](#asmacdo)
**Repository**: [github.com/asmacdo/asmacdo-duct-gallery](https://github.com/asmacdo/asmacdo-duct-gallery/)
-
+
+| ps pcpu (raw) | ps cpu (time-point estimate) |
+ |  |
+
📋 Metadata
@@ -99,15 +108,18 @@ Demo example from the con/duct repository showing resource usage tracking
**Tags**: [`local`](#local) [`s5cmd`](#s5cmd)
-
+
+| ps pcpu (raw) | ps cpu (time-point estimate) |
+ |  |
+
📋 Metadata
-- **Info file**: [example_output_info.json](/home/runner/work/duct-gallery/duct-gallery/logs/s5cmd-1/2024.10.28T11.08.51-2733714_info.json)
-- **Usage data**: [example_output_usage.json](/home/runner/work/duct-gallery/duct-gallery/logs/s5cmd-1/2024.10.28T11.08.51-2733714_usage.json)
-- **Standard output**: [stdout](/home/runner/work/duct-gallery/duct-gallery/logs/s5cmd-1/2024.10.28T11.08.51-2733714_stdout)
-- **Standard error**: [stderr](/home/runner/work/duct-gallery/duct-gallery/logs/s5cmd-1/2024.10.28T11.08.51-2733714_stderr)
+- **Info file**: [example_output_info.json](/home/austin/devel/duct-gallery/.worktrees/plot-pdcpu-gallery-preview/logs/s5cmd-1/2024.10.28T11.08.51-2733714_info.json)
+- **Usage data**: [example_output_usage.json](/home/austin/devel/duct-gallery/.worktrees/plot-pdcpu-gallery-preview/logs/s5cmd-1/2024.10.28T11.08.51-2733714_usage.json)
+- **Standard output**: [stdout](/home/austin/devel/duct-gallery/.worktrees/plot-pdcpu-gallery-preview/logs/s5cmd-1/2024.10.28T11.08.51-2733714_stdout)
+- **Standard error**: [stderr](/home/austin/devel/duct-gallery/.worktrees/plot-pdcpu-gallery-preview/logs/s5cmd-1/2024.10.28T11.08.51-2733714_stderr)
@@ -118,7 +130,10 @@ Demo example from the con/duct repository showing resource usage tracking
**Tags**: [`mriqc`](#mriqc) [`juelich`](#juelich)
**Repository**: [cerebra.fz-juelich.de/f.hoffstaedter/ds005256-mriqc](https://cerebra.fz-juelich.de/f.hoffstaedter/ds005256-mriqc)
-
+
+| ps pcpu (raw) | ps cpu (time-point estimate) |
+ |  |
+
📋 Metadata
diff --git a/con-duct-gallery.yaml b/con-duct-gallery.yaml
index c7c69be..0ebdb63 100644
--- a/con-duct-gallery.yaml
+++ b/con-duct-gallery.yaml
@@ -1,3 +1,11 @@
+variants:
+ - name: ps-pcpu
+ label: "ps pcpu (raw)"
+ plot_options: ["--cpu", "ps-pcpu"]
+ - name: ps-cpu-timepoint
+ label: "ps cpu (time-point estimate)"
+ plot_options: ["--cpu", "ps-cpu-timepoint"]
+
examples:
- title: "con/duct Demo Example"
source_repo: "https://github.com/con/duct/"
diff --git a/images/asmacdo-gallery-example-1__ps-cpu-timepoint.svg b/images/asmacdo-gallery-example-1__ps-cpu-timepoint.svg
new file mode 100644
index 0000000..693b6f9
--- /dev/null
+++ b/images/asmacdo-gallery-example-1__ps-cpu-timepoint.svg
@@ -0,0 +1,2020 @@
+
+
+
diff --git a/images/asmacdo-gallery-example-1__ps-pcpu.svg b/images/asmacdo-gallery-example-1__ps-pcpu.svg
new file mode 100644
index 0000000..76ec71e
--- /dev/null
+++ b/images/asmacdo-gallery-example-1__ps-pcpu.svg
@@ -0,0 +1,2256 @@
+
+
+
diff --git a/images/asmacdo-gallery-example-2.svg b/images/asmacdo-gallery-example-2.svg
deleted file mode 100644
index fc8d161..0000000
--- a/images/asmacdo-gallery-example-2.svg
+++ /dev/null
@@ -1,1758 +0,0 @@
-
-
-
diff --git a/images/asmacdo-gallery-example-1.svg b/images/asmacdo-gallery-example-2__ps-cpu-timepoint.svg
similarity index 50%
rename from images/asmacdo-gallery-example-1.svg
rename to images/asmacdo-gallery-example-2__ps-cpu-timepoint.svg
index df015a7..fde9d99 100644
--- a/images/asmacdo-gallery-example-1.svg
+++ b/images/asmacdo-gallery-example-2__ps-cpu-timepoint.svg
@@ -6,11 +6,11 @@
- 2025-10-03T14:08:06.083291
+ 2026-05-06T12:29:57.082453
image/svg+xml
- Matplotlib v3.10.6, https://matplotlib.org/
+ Matplotlib v3.10.9, https://matplotlib.org/
@@ -30,10 +30,10 @@ z
-
@@ -41,17 +41,17 @@ z
-
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
-
+
-
+
@@ -605,12 +605,12 @@ L -3.5 0
-
+
-
+
@@ -622,12 +622,12 @@ L -3.5 0
-
+
-
+
@@ -639,12 +639,12 @@ L -3.5 0
-
+
-
+
-
+
-
+
@@ -685,12 +685,12 @@ z
-
+
-
+
@@ -702,12 +702,12 @@ z
-
+
-
+
@@ -719,12 +719,12 @@ z
-
+
-
+
@@ -736,12 +736,12 @@ z
-
+
-
+
@@ -751,45 +751,14 @@ z
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+" transform="scale(0.015625)"/>
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
@@ -1277,356 +1353,483 @@ z
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
+
-
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
+
-
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
-
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
+
+
diff --git a/images/asmacdo-gallery-example-2__ps-pcpu.svg b/images/asmacdo-gallery-example-2__ps-pcpu.svg
new file mode 100644
index 0000000..ce2c7a6
--- /dev/null
+++ b/images/asmacdo-gallery-example-2__ps-pcpu.svg
@@ -0,0 +1,2139 @@
+
+
+
diff --git a/images/con-duct-demo-example.svg b/images/con-duct-demo-example.svg
deleted file mode 100644
index 409544f..0000000
--- a/images/con-duct-demo-example.svg
+++ /dev/null
@@ -1,1662 +0,0 @@
-
-
-
diff --git a/images/con-duct-demo-example__ps-cpu-timepoint.svg b/images/con-duct-demo-example__ps-cpu-timepoint.svg
new file mode 100644
index 0000000..b7ca7b0
--- /dev/null
+++ b/images/con-duct-demo-example__ps-cpu-timepoint.svg
@@ -0,0 +1,2428 @@
+
+
+
diff --git a/images/con-duct-demo-example__ps-pcpu.svg b/images/con-duct-demo-example__ps-pcpu.svg
new file mode 100644
index 0000000..8912458
--- /dev/null
+++ b/images/con-duct-demo-example__ps-pcpu.svg
@@ -0,0 +1,2334 @@
+
+
+
diff --git a/images/mriqc-processing-on-a-single-subject-session.svg b/images/mriqc-processing-on-a-single-subject-session.svg
deleted file mode 100644
index 24bbea1..0000000
--- a/images/mriqc-processing-on-a-single-subject-session.svg
+++ /dev/null
@@ -1,2957 +0,0 @@
-
-
-
diff --git a/images/mriqc-processing-on-a-single-subject-session__ps-cpu-timepoint.svg b/images/mriqc-processing-on-a-single-subject-session__ps-cpu-timepoint.svg
new file mode 100644
index 0000000..8bfd848
--- /dev/null
+++ b/images/mriqc-processing-on-a-single-subject-session__ps-cpu-timepoint.svg
@@ -0,0 +1,12742 @@
+
+
+
diff --git a/images/mriqc-processing-on-a-single-subject-session__ps-pcpu.svg b/images/mriqc-processing-on-a-single-subject-session__ps-pcpu.svg
new file mode 100644
index 0000000..a3aa525
--- /dev/null
+++ b/images/mriqc-processing-on-a-single-subject-session__ps-pcpu.svg
@@ -0,0 +1,15270 @@
+
+
+
diff --git a/images/s5cmd-sync-dry-invocation-on-a-mighty-dandiarchive-bucket.svg b/images/s5cmd-sync-dry-invocation-on-a-mighty-dandiarchive-bucket.svg
deleted file mode 100644
index 57adb5a..0000000
--- a/images/s5cmd-sync-dry-invocation-on-a-mighty-dandiarchive-bucket.svg
+++ /dev/null
@@ -1,2733 +0,0 @@
-
-
-
diff --git a/images/s5cmd-sync-dry-invocation-on-a-mighty-dandiarchive-bucket__ps-cpu-timepoint.svg b/images/s5cmd-sync-dry-invocation-on-a-mighty-dandiarchive-bucket__ps-cpu-timepoint.svg
new file mode 100644
index 0000000..9b911f5
--- /dev/null
+++ b/images/s5cmd-sync-dry-invocation-on-a-mighty-dandiarchive-bucket__ps-cpu-timepoint.svg
@@ -0,0 +1,3567 @@
+
+
+
diff --git a/images/s5cmd-sync-dry-invocation-on-a-mighty-dandiarchive-bucket__ps-pcpu.svg b/images/s5cmd-sync-dry-invocation-on-a-mighty-dandiarchive-bucket__ps-pcpu.svg
new file mode 100644
index 0000000..728db84
--- /dev/null
+++ b/images/s5cmd-sync-dry-invocation-on-a-mighty-dandiarchive-bucket__ps-pcpu.svg
@@ -0,0 +1,3069 @@
+
+
+
diff --git a/src/con_duct_gallery/__main__.py b/src/con_duct_gallery/__main__.py
index 24d8f51..9b94ebf 100644
--- a/src/con_duct_gallery/__main__.py
+++ b/src/con_duct_gallery/__main__.py
@@ -68,6 +68,9 @@ def main() -> int:
plot_failures = 0
example_log_paths = {} # Store log paths for each example
+ variants = registry.variants
+ use_variants = len(variants) >= 2
+
for example in registry.examples:
try:
# Fetch logs
@@ -81,24 +84,50 @@ def main() -> int:
'stderr': fetched_log.stderr
}
- # Generate plot if needed
slug = slugify(example.title)
- svg_path = args.image_dir / f"{slug}.svg"
-
- if should_regenerate_plot(svg_path, fetched_log.usage_json, args.force):
- try:
- logger.info(f"Generating plot for '{example.title}'")
- generate_plot(
- fetched_log.usage_json,
- svg_path,
- example.plot_options
- )
- logger.info(f" ✓ Plot saved: {svg_path}")
- except Exception as e:
- logger.warning(f" ✗ Plot generation failed: {e}")
- plot_failures += 1
+
+ if use_variants:
+ for variant in variants:
+ svg_path = args.image_dir / f"{slug}__{variant.name}.svg"
+ if should_regenerate_plot(svg_path, fetched_log.usage_json, args.force):
+ try:
+ logger.info(
+ f"Generating plot for '{example.title}' "
+ f"[variant: {variant.name}]"
+ )
+ # Variant options come first; example-level options
+ # follow so they can override if needed.
+ plot_opts = list(variant.plot_options) + list(example.plot_options)
+ generate_plot(
+ fetched_log.usage_json,
+ svg_path,
+ plot_opts,
+ )
+ logger.info(f" ✓ Plot saved: {svg_path}")
+ except Exception as e:
+ logger.warning(f" ✗ Plot generation failed: {e}")
+ plot_failures += 1
+ else:
+ logger.info(
+ f"Using cached plot for '{example.title}' "
+ f"[variant: {variant.name}]"
+ )
else:
- logger.info(f"Using cached plot for '{example.title}'")
+ svg_path = args.image_dir / f"{slug}.svg"
+ if should_regenerate_plot(svg_path, fetched_log.usage_json, args.force):
+ try:
+ logger.info(f"Generating plot for '{example.title}'")
+ generate_plot(
+ fetched_log.usage_json,
+ svg_path,
+ example.plot_options
+ )
+ logger.info(f" ✓ Plot saved: {svg_path}")
+ except Exception as e:
+ logger.warning(f" ✗ Plot generation failed: {e}")
+ plot_failures += 1
+ else:
+ logger.info(f"Using cached plot for '{example.title}'")
except Exception as e:
logger.warning(f"✗ Failed to fetch '{example.title}': {e}")
diff --git a/src/con_duct_gallery/generator.py b/src/con_duct_gallery/generator.py
index c145904..f7f3495 100644
--- a/src/con_duct_gallery/generator.py
+++ b/src/con_duct_gallery/generator.py
@@ -4,7 +4,7 @@
from datetime import datetime
from pathlib import Path
-from .models import ExampleEntry, ExampleRegistry
+from .models import ExampleEntry, ExampleRegistry, PlotVariant
def slugify(title: str) -> str:
@@ -82,15 +82,19 @@ def generate_example_section(
example: ExampleEntry,
svg_exists: bool,
log_paths: dict[str, Path],
- image_dir: str
+ image_dir: str,
+ variants: list[PlotVariant] = None,
+ variant_svg_exists: dict[str, bool] = None,
) -> str:
"""Generate markdown section for a single example.
Args:
example: Example entry
- svg_exists: Whether SVG plot file exists
+ svg_exists: Whether the single-plot SVG file exists (when variants is None/empty)
log_paths: Dictionary with 'info', 'usage', 'stdout', 'stderr' paths
image_dir: Directory containing image files
+ variants: Plot variants to render side-by-side (if 2+ provided)
+ variant_svg_exists: Map from variant name to whether its SVG exists
Returns:
Markdown section for the example
@@ -116,12 +120,34 @@ def generate_example_section(
lines.append(example.description)
lines.append("")
- # Plot image or warning
slug = slugify(example.title)
- if svg_exists:
- lines.append(f"")
+
+ if variants and len(variants) >= 2:
+ # Side-by-side variants. GitHub renders inline HTML tables in markdown.
+ variant_svg_exists = variant_svg_exists or {}
+ lines.append("")
+ header_cells = "".join(
+ f"| {v.display_label} | " for v in variants
+ )
+ lines.append(f"{header_cells}
")
+ body_cells = []
+ for v in variants:
+ if variant_svg_exists.get(v.name, False):
+ body_cells.append(
+ f" | "
+ )
+ else:
+ body_cells.append(
+ "⚠️ Plot not available | "
+ )
+ lines.append(f"{''.join(body_cells)}
")
+ lines.append("
")
else:
- lines.append("> ⚠️ **Plot not available** - Generation failed or plot file missing")
+ if svg_exists:
+ lines.append(f"")
+ else:
+ lines.append("> ⚠️ **Plot not available** - Generation failed or plot file missing")
lines.append("")
@@ -187,20 +213,37 @@ def generate_gallery(
sections.append("## 📊 Examples\n")
# Generate section for each example
+ variants = registry.variants
+ use_variants = len(variants) >= 2
+
for example in registry.examples:
slug = slugify(example.title)
- svg_path = image_dir / f"{slug}.svg"
- svg_exists = svg_path.exists()
# Get log paths for this example
log_paths = example_log_paths.get(example.title, {})
- section = generate_example_section(
- example,
- svg_exists,
- log_paths=log_paths,
- image_dir=str(image_dir)
- )
+ if use_variants:
+ variant_svg_exists = {
+ v.name: (image_dir / f"{slug}__{v.name}.svg").exists()
+ for v in variants
+ }
+ section = generate_example_section(
+ example,
+ svg_exists=False,
+ log_paths=log_paths,
+ image_dir=str(image_dir),
+ variants=variants,
+ variant_svg_exists=variant_svg_exists,
+ )
+ else:
+ svg_path = image_dir / f"{slug}.svg"
+ section = generate_example_section(
+ example,
+ svg_exists=svg_path.exists(),
+ log_paths=log_paths,
+ image_dir=str(image_dir),
+ )
+
sections.append(section)
sections.append("---\n") # Separator
diff --git a/src/con_duct_gallery/models.py b/src/con_duct_gallery/models.py
index 96ae9ef..445e05f 100644
--- a/src/con_duct_gallery/models.py
+++ b/src/con_duct_gallery/models.py
@@ -70,10 +70,42 @@ def is_local(self) -> bool:
return not str(self.info_file).startswith('http')
+class PlotVariant(BaseModel):
+ """A named plot variant rendered for every example.
+
+ When the registry has 2+ variants, each example renders one SVG per
+ variant and they are laid out side-by-side in the README. The variant
+ `name` is used in the SVG filename (`__.svg`); `label` is
+ the human-readable column header.
+ """
+
+ name: str
+ label: str = ""
+ plot_options: list[str] = []
+
+ @field_validator('name')
+ @classmethod
+ def validate_name(cls, v: str) -> str:
+ v = v.strip()
+ if not v:
+ raise ValueError('Variant name cannot be empty')
+ # Filename-safe: alphanumeric + hyphens
+ if not v.replace('-', '').replace('_', '').isalnum():
+ raise ValueError(
+ f'Variant name "{v}" must be alphanumeric + hyphens/underscores only'
+ )
+ return v
+
+ @property
+ def display_label(self) -> str:
+ return self.label if self.label else self.name
+
+
class ExampleRegistry(BaseModel):
"""Collection of all examples, loaded from YAML configuration."""
examples: list[ExampleEntry]
+ variants: list[PlotVariant] = []
@field_validator('examples')
@classmethod