Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We used a venv for testing.
This can be removed if necessary

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't exactly understand the causal link between these two.

In any case, you should rebase your branch on the current main branch, resolve possible conflicts, and update the pull request.

Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ src/cocopp/**/*.dat
src/cocopp/**/*.tdat
src/cocopp/**/*.info
dist/
ppdata/
ppdata/
venv/
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ The package uses data generated with the [COCO framework](https://coco-platform.

The main documentation pages for the `coco-postprocess` package `cocopp` can be found at


- [getting-started](https://numbbo.it/getting-started#postprocess)
- [API documentation](https://numbbo.github.io/coco-doc/apidocs/cocopp)
- [Issue tracker and bug reports](https://github.com/numbbo/coco-postprocess/issues)
Expand Down
87 changes: 87 additions & 0 deletions src/cocopp/html/generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""HTML content generator for COCO post-processing."""

import json
import os

class HtmlGenerator:
"""Generates HTML content with dynamic JavaScript updates."""

STATIC_TEMPLATE = """<!DOCTYPE html>
<HTML>
<HEAD>
<META NAME="description" CONTENT="COCO/BBOB figures by function">
<META NAME="keywords" CONTENT="COCO, BBOB">
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8">
<TITLE>COCO Post-Processing Results</TITLE>
<SCRIPT SRC="sorttable.js"></SCRIPT>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
h1 { color: #333; }
h2 { color: #666; margin-top: 30px; }
a { color: #0066cc; text-decoration: none; }
a:hover { text-decoration: underline; }
#linksContainer { margin: 20px 0; }
.link-item { margin: 8px 0; }
.nav-link { font-weight: bold; margin: 5px 0; }
</style>
</HEAD>
<BODY>
<H1>COCO Post-Processing Results</H1>
<div id="linksContainer"></div>
<div id="imagesContainer"></div>

<script>
// Data injected by server
window.contentData = {data};
</script>
<SCRIPT SRC="renderer.js"></SCRIPT>

</BODY>
</HTML>"""

def __init__(self):
pass

def generate_parent_index_data(self, algo_data, single_file_name, many_file_name):
"""Generate data structure for parent index."""
return {
'title': 'COCO Post-Processing Results',
'header': 'COCO Post-Processing Results',
'nav_links': [],
'single': sorted(algo_data.get('single', [])),
'comparison': sorted(algo_data.get('comparison', [])),
'images': [],
'single_file_name': single_file_name,
'many_file_name': many_file_name
}

def generate_folder_content(self, current_dir, image_extension):
"""Generate data structure for folder index."""
nav_links = [
'<a href="../index.html">Home</a>',
'<a href="pprldflex.html">Runtime profiles (with arrow keys navigation)</a>',
'<a href="pptable.html">Tables for selected targets</a>',
'<a href="pprldistr.html">Runtime profiles for selected targets</a>'
]

images = []
image_path = 'pprldmany-single-functions/pprldmany.%s' % image_extension
if os.path.isfile(os.path.join(current_dir, image_path)):
images.append(image_path)

return {
'title': 'COCO Post-Processing Results',
'header': 'Results Overview',
'nav_links': nav_links,
'single': [],
'comparison': [],
'images': images,
'single_file_name': '',
'many_file_name': ''
}

def render(self, data):
"""Render HTML with injected data."""
json_data = json.dumps(data, ensure_ascii=False)
html = self.STATIC_TEMPLATE.replace('{data}', json_data)
return html
116 changes: 116 additions & 0 deletions src/cocopp/html/index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""Main interface for generating folder index pages."""

import logging
import os
from .generator import HtmlGenerator
from .writer import HtmlWriter
from .. import genericsettings

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def collect_algorithm_data(directory):
"""Collect algorithm data from directory structure."""
data = {'single': [], 'comparison': []}

if not os.path.isdir(directory):
return data

try:
for item in os.listdir(directory):
item_path = os.path.join(directory, item)
if not os.path.isdir(item_path):
continue

single_file = os.path.join(item_path, '%s.html' % genericsettings.single_algorithm_file_name)
if os.path.isfile(single_file):
data['single'].append(item)

many_file = os.path.join(item_path, '%s.html' % genericsettings.many_algorithm_file_name)
if os.path.isfile(many_file):
data['comparison'].append(item)
except OSError as e:
logger.warning("Error reading directory %s: %s" % (directory, str(e)))

return data

def update_parent_index(parent_index_path):
"""Update the parent index.html file with links to algorithm results.

This function only writes the HTML file once. Links are managed
dynamically via JavaScript based on available algorithm directories.

Args:
parent_index_path: Path to the parent index.html file

Raises:
IOError: If there are issues reading/writing the index file
"""
try:
generator = HtmlGenerator()
parent_dir = os.path.dirname(os.path.realpath(parent_index_path))

# to collect data from available algorithms
algo_data = collect_algorithm_data(parent_dir)

# generating data structure for dynamic rendering
data = generator.generate_parent_index_data(
algo_data,
genericsettings.single_algorithm_file_name,
genericsettings.many_algorithm_file_name
)

# HTML rendering
html = generator.render(data)

# initial creation of the file (will be created only if it doesn't already exist)
writer = HtmlWriter()
if not os.path.isfile(parent_index_path):
writer.write_safely(parent_index_path, html)
logger.info("Created parent index at %s" % parent_index_path)
else:
logger.info("Parent index already exists at %s (using dynamic JS updates)" % parent_index_path)

except Exception as e:
logger.error("Failed to update parent index: %s" % str(e))
raise IOError("Failed to update parent index: %s" % str(e))

def save_folder_index(filepath, image_extension):
"""Generate and save a folder index file.

The HTML file is created once with static structure. Dynamic content
is managed via JavaScript and server-side data updates.

Args:
filepath: Path where the index file should be saved
image_extension: Extension for image files (e.g. 'svg', 'png')
"""
if not filepath:
return

try:
# content data generation
generator = HtmlGenerator()
current_dir = os.path.dirname(os.path.realpath(filepath))
data = generator.generate_folder_content(current_dir, image_extension)

# rendering to HTML
html = generator.render(data)

# initial creation of the file (created only if it doesn't already exist)
writer = HtmlWriter()
if not os.path.isfile(filepath):
writer.write_safely(filepath, html)
logger.info("Created folder index at %s" % filepath)
else:
logger.info("Folder index already exists at %s (using dynamic JS updates)" % filepath)

# update parent index if needed
parent_dir = os.path.dirname(current_dir)
parent_index_path = os.path.join(parent_dir, 'index.html')
if not os.path.isfile(parent_index_path):
update_parent_index(parent_index_path)

except Exception as e:
logger.error("Failed to save folder index at %s: %s" % (filepath, str(e)))
raise
57 changes: 57 additions & 0 deletions src/cocopp/html/renderer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// data injected by server
var contentData = window.contentData || {};

function renderLinks(data) {
var container = document.getElementById('linksContainer');
var html = '';

// Navigation links
if (data.nav_links && data.nav_links.length > 0) {
data.nav_links.forEach(function(link) {
if (link) {
html += '<div class="nav-link">' + link + '</div>';
}
});
}

// comparison code
if (data.comparison && data.comparison.length > 0) {
html += '<h2>Comparison Data</h2>';
data.comparison.forEach(function(algo) {
var path = algo + '/' + data.many_file_name + '.html';
html += '<div class="link-item">&nbsp;&nbsp;<a href="' + path + '">' + algo + '</a></div>';
});
}

// single algorithm code
if (data.single && data.single.length > 0) {
html += '<h2>Single Algorithm Data</h2>';
data.single.forEach(function(algo) {
var path = algo + '/' + data.single_file_name + '.html';
html += '<div class="link-item">&nbsp;&nbsp;<a href="' + path + '">' + algo + '</a></div>';
});
}

container.innerHTML = html;
}

function renderImages(data) {
var container = document.getElementById('imagesContainer');
var html = '';

if (data.images && data.images.length > 0) {
data.images.forEach(function(img) {
html += '<div><a href="' + img + '"><img src="' + img + '" height="380em"></a></div>';
});
}

container.innerHTML = html;
}

// rendering code
document.addEventListener('DOMContentLoaded', function() {
if (contentData) {
renderLinks(contentData);
renderImages(contentData);
}
});
29 changes: 29 additions & 0 deletions src/cocopp/html/writer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Safe file writing utilities for HTML output."""

import os

class HtmlWriter:
"""Handles safe writing of HTML files with atomic operations."""

@staticmethod
def write_safely(filepath, content):
"""Write content to file atomically using a temporary file.

Args:
filepath: Path to the output file
content: HTML content to write
"""
filepath = str(filepath)

# creating parent directories (if they don't already exist)
parent_dir = os.path.dirname(filepath)
if parent_dir and not os.path.exists(parent_dir):
os.makedirs(parent_dir)

try:
with open(filepath, 'w', encoding='utf-8', newline='') as f:
f.write(content)
f.flush()
os.fsync(f.fileno()) # forcing writing to disk (for mac users)
except Exception as e:
raise IOError("Failed to write %s: %s" % (filepath, str(e)))
4 changes: 3 additions & 1 deletion src/cocopp/ppfig.py
Copy link
Author

@joshua2705 joshua2705 Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the line mentioned in the issue #17 (line no 499) which gets called repeatedly by the old architecture.
We removed this line and replaced it with a function which changes the architecture.

The function to this new line is imported in line 17.

Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
from matplotlib import pyplot as plt
import shutil
from six import advance_iterator
from cocopp.html.index import save_folder_index

# from pdb import set_trace

# absolute_import => . refers to where ppfig resides in the package:
Expand Down Expand Up @@ -496,7 +498,7 @@ def save_single_functions_html(filename,
toolsdivers.replace_in_file(filename + add_to_names + '.html', '??COCOVERSION??', '<br />Data produced with COCO %s' % (toolsdivers.get_version_label(None)))

if parentFileName:
save_folder_index_file(os.path.join(current_dir, parentFileName + '.html'), extension)
save_folder_index(os.path.join(current_dir, parentFileName + '.html'), extension)


def write_dimension_links(dimension, dimensions, index):
Expand Down
Binary file added src/cocopp/refalgs/best2009-bbob.tar.gz.pickle
Binary file not shown.