← Back to Workflow
Skill

Apt Analogies Templates Skill

Contains master code templates for generating timeline histograms and HTML output visualizations for the analogies database.

Apt Analogies Templates: Histogram & HTML Formatting

This skill contains the master code templates for generating output visualizations for the analogies database. When the apt-analogies-from-history-literature-movies workflow instructs you to create generate_histogram.py or build_html.py, base your code on these templates.

1. Histogram Formatter

When creating generate_histogram.py, use the following Python boilerplate to ensure exact timeline bucketing and medium consolidation.

import json
from collections import Counter

def get_era_bucket(year):
    try:
        y = int(year)
    except:
        return "Unknown"
        
    if y >= 1940:
        return f"{(y // 10) * 10}s"
    elif y >= 1900:
        return "1900 to 1940"
    elif y >= 1500:
        return f"{(y // 100) * 100}s"
    elif y >= 1000:
        return "1000 to 1500"
    elif y >= 500:
        return "500 to 1000"
    elif y >= 0:
        return "0 to 500"
    elif y >= -500:
        return "500 BC to Year 0"
    else:
        return "Before 500 BC"

def get_medium_bucket(medium):
    m = medium.lower()
    if 'history' in m:
        return 'History'
    elif 'literature' in m or 'theater' in m or 'lore' in m:
        return 'Literature'
    elif 'film' in m or 'tv' in m or 'movie' in m or 'animation' in m:
        return 'Movies/TV'
    else:
        return 'Other'

def generate_histogram():
    with open('analogies.json', 'r', encoding='utf-8') as f:
        data = json.load(f)

    included = [e for e in data.get('entries', []) if e.get('status') == 'included']
    total = len(included)
    if total == 0:
        print("No included entries to chart.")
        return

    # Mediums
    mediums = [get_medium_bucket(e.get('medium', '')) for e in included]
    medium_counts = Counter(mediums)

    # Eras
    eras = [get_era_bucket(e.get('year')) for e in included]
    era_counts = Counter(eras)

    # Era Sort Order strictly mapping the historical buckets
    era_order = [
         "2020s", "2010s", "2000s", "1990s", "1980s", "1970s", "1960s", "1950s", "1940s",
         "1900 to 1940",
         "1800s", "1700s", "1600s", "1500s",
         "1000 to 1500",
         "500 to 1000",
         "0 to 500",
         "500 BC to Year 0",
         "Before 500 BC",
         "Unknown"
    ]

    out_blocks = []
    
    out_blocks.append("=== HISTOGRAM 1: MEDIUMS ===")
    for m in ['History', 'Literature', 'Movies/TV', 'Other']:
        if medium_counts[m] > 0 or m != 'Other': 
            c = medium_counts[m]
            bar = '#' * c
            line = f"{m.ljust(15)} | {str(c).rjust(2)} ({c/total*100:4.1f}%) | {bar}"
            print(line)
            out_blocks.append(line)

    out_blocks.append("\n=== HISTOGRAM 2: ERAS ===")
    for era in era_order:
        if era in era_counts or any(era_counts.get(e, 0) > 0 for e in era_counts):
            c = era_counts.get(era, 0)
            bar = '#' * c
            line = f"{era.ljust(17)} | {str(c).rjust(2)} ({c/total*100:4.1f}%) | {bar}"
            print(line)
            out_blocks.append(line)

    # Automatically save it so the user can easily see it locally in markdown format
    with open('histogram.md', 'w', encoding='utf-8') as f:
        f.write("# Database Histogram Distributions\n\n```text\n")
        f.write("\n".join(out_blocks))
        f.write("\n```\n")

if __name__ == '__main__':
    generate_histogram()

2. HTML Formatter

When creating build_html.py, use exactly the following HTML/CSS boilerplate and row-container logic to inject the visual styles. This ensures output consistency across all instances of the analogies database.

    html_template = """<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{title}</title>
    <style>
        body {
            background-color: #ffffff;
            color: #000000;
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 40px;
        }
        h1 {
            font-size: 28px;
            color: #000000;
            text-transform: uppercase;
            border-bottom: 3px solid #000000;
            padding-bottom: 10px;
            margin-bottom: 30px;
        }
        .nav-links {
            margin-bottom: 30px;
            font-size: 16px;
            font-weight: bold;
        }
        .nav-links a {
            color: #0000EE;
            margin-right: 20px;
            text-decoration: none;
        }
        .nav-links a:hover { text-decoration: underline; }
        .row-container {
            border: 2px solid #000000;
            margin-bottom: 30px;
            padding: 20px;
            display: flex;
            flex-direction: column;
            gap: 20px;
        }
        .row-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            border-bottom: 1px solid #cccccc;
            padding-bottom: 15px;
        }
        .row-header h2 {
            margin: 0;
            font-size: 24px;
            color: #000000;
        }
        .meta {
            font-size: 14px;
            color: #555555;
            font-weight: bold;
            margin-top: 5px;
            display: block;
        }
        .score-box {
            font-size: 24px;
            font-weight: bold;
            color: #000000;
            border: 2px solid #000000;
            padding: 10px 15px;
        }
        .penalty-text {
            color: #cc0000;
            font-size: 14px;
            display: block;
            margin-top: 5px;
        }
        .exclusion-box {
            background-color: #ffeeee;
            border: 2px solid #cc0000;
            padding: 15px;
            color: #cc0000;
            font-size: 16px;
        }
        .row-body {
            display: flex;
            flex-direction: column;
            gap: 20px;
        }
        .element-grid {
            display: grid;
            grid-template-columns: 250px 1fr;
            gap: 15px 20px;
        }
        .el-label {
            font-weight: bold;
            color: #000000;
        }
        .el-value {
            color: #222222;
            line-height: 1.4;
        }
        .el-value ul {
            margin: 0;
            padding-left: 20px;
        }
        .el-value li {
            margin-bottom: 5px;
        }
        @media (max-width: 1000px) {
            .element-grid {
                grid-template-columns: 1fr;
            }
        }
    </style>
</head>
<body>
    <div class="nav-links">
        <a href="included_analogies.html">View Included (Master Database)</a>
        <a href="excluded_analogies.html">View Excluded (Graveyard)</a>
    </div>
    <h1>{title}</h1>
    <div>
{content}
    </div>
</body>
</html>"""

    def render_entry(entry, is_excluded=False):
        content = '<div class="row-container">'
        
        # Header
        content += '<div class="row-header"><div class="title-group">'
        content += f'<h2>{entry.get("title", "Unknown")}</h2>'
        
        # NOTE: Handle specific schema if elements differ (year/era)
        year_or_era = entry.get("year", entry.get("era", "Unknown"))
        content += f'<span class="meta">({year_or_era}) | {entry.get("medium", "Unknown")}</span>'
        content += '</div>'
        
        # Score Box
        content += '<div class="score-box" style="text-align: right;">'
        score_key = "score" if "score" in entry else "total_score"
        content += f'Score: {entry.get(score_key, 0)}'
        penalties = entry.get("penalties", [])
        if penalties:
            p_text = []
            for p in penalties:
                points = p.get('points', p.get('amount', 0))
                p_text.append(f"{p.get('name')} ({points})")
            penalty_str = ", ".join(p_text)
            content += f'<span class="penalty-text">Penalties: {penalty_str}</span>'
        content += '</div></div>'

        content += '<div class="row-body">'
        
        if is_excluded and entry.get("exclusion_reason"):
            content += f'<div class="exclusion-box"><strong>Excluded:</strong> {entry.get("exclusion_reason")}</div>'
            
        content += '<div class="element-grid">'
        
        elements = entry.get("elements", [])
        # Determine schema structure: list of dicts vs dict of dicts
        if isinstance(elements, dict):
            for name, data in elements.items():
                content += f'<div class="el-label">{name}</div>'
                bullets = [data.get("description", "")]
                val_html = '<ul>' + ''.join(f'<li>{b}</li>' for b in bullets if b) + '</ul>' if bullets and bullets[0] else ''
                content += f'<div class="el-value">{val_html}</div>'
        else:
            import re
            for el in elements:
                raw_name = el.get("name", "")
                if "Element 4" in raw_name or "Threatener is Evil" in raw_name: # Handle legacy modifiers if present
                    continue
                clean_name = re.sub(r'^Element \d+:\s*', '', raw_name)
                content += f'<div class="el-label">{clean_name}</div>'
                bullets = el.get("explanation_bullets", [])
                val_html = '<ul>' + ''.join(f'<li>{b}</li>' for b in bullets) + '</ul>' if bullets else ''
                content += f'<div class="el-value">{val_html}</div>'
            
        content += '</div></div></div>\n'
        return content

This is used in: