feat(battlecard): implement mini-chart engine with 4 chart templates (line trend, horizontal bar, utilization gauge, comparison bar)
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 45 KiB |
@@ -1,211 +1,338 @@
|
|||||||
"""Mini-chart engine for battle card visualizations.
|
"""Mini-chart engine for battle card embeddings."""
|
||||||
|
|
||||||
Generates compact, themed 5x3 inch charts suitable for embedding
|
|
||||||
in FIA battle cards. Uses the existing project styling utilities.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import matplotlib
|
import matplotlib
|
||||||
matplotlib.use("Agg")
|
matplotlib.use("Agg")
|
||||||
import matplotlib.figure # noqa: E402
|
import matplotlib.pyplot as plt
|
||||||
import matplotlib.pyplot as plt # noqa: E402
|
import matplotlib.patches as mpatches
|
||||||
|
from matplotlib.path import Path as MPLPath
|
||||||
|
# Python 3.14 matplotlib patch (required)
|
||||||
|
_orig = MPLPath.__deepcopy__
|
||||||
|
def _safe_deepcopy(self, memo):
|
||||||
|
if id(self) in memo:
|
||||||
|
return memo[id(self)]
|
||||||
|
memo[id(self)] = self
|
||||||
|
return self
|
||||||
|
MPLPath.__deepcopy__ = _safe_deepcopy
|
||||||
|
|
||||||
from src.utils.styling import ( # noqa: E402
|
from pathlib import Path
|
||||||
AI_SPEND,
|
|
||||||
BUBBLE_ZONE,
|
from src.utils.styling import (
|
||||||
REVENUE,
|
|
||||||
GRAY_DARK,
|
|
||||||
WHITE,
|
|
||||||
EXPORT_DPI,
|
|
||||||
get_theme,
|
get_theme,
|
||||||
apply_theme,
|
EXPORT_DPI,
|
||||||
|
BUBBLE_ZONE,
|
||||||
|
AI_SPEND,
|
||||||
|
REVENUE,
|
||||||
|
WARNING_ZONE,
|
||||||
|
NORMAL_ZONE,
|
||||||
|
GRAY_DARK,
|
||||||
|
GRAY_MEDIUM,
|
||||||
|
WHITE,
|
||||||
)
|
)
|
||||||
|
from src.utils.export import save_chart
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
MINI_FIGURE_SIZE = (5, 3)
|
||||||
# Sizing constants
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
MINI_FIGURE_SIZE = (5, 3) # width, height in inches
|
|
||||||
MINI_DPI = 300
|
MINI_DPI = 300
|
||||||
|
|
||||||
MIN_LABEL_FONT_SIZE = 9
|
MIN_LABEL_FONT_SIZE = 9
|
||||||
MIN_ANNOTATION_FONT_SIZE = 11
|
MIN_ANNOTATION_FONT_SIZE = 11
|
||||||
MIN_TITLE_FONT_SIZE = 13
|
MIN_TITLE_FONT_SIZE = 13
|
||||||
|
|
||||||
|
|
||||||
class MiniChartEngine:
|
class MiniChartEngine:
|
||||||
"""Engine for generating compact, themed mini-charts for battle cards.
|
"""Engine for generating compact, themed mini-charts for battle cards."""
|
||||||
|
|
||||||
Each method creates a 5x3 inch figure, applies the project theme,
|
def _init_figure(self, title: str):
|
||||||
draws the requested chart type, and saves as a PNG at 300 DPI.
|
"""Create a themed figure and axes."""
|
||||||
"""
|
plt.rcParams.update(get_theme())
|
||||||
|
fig, ax = plt.subplots(figsize=MINI_FIGURE_SIZE)
|
||||||
|
fig.set_facecolor(WHITE)
|
||||||
|
ax.set_facecolor(WHITE)
|
||||||
|
ax.spines["top"].set_visible(False)
|
||||||
|
ax.spines["right"].set_visible(False)
|
||||||
|
ax.spines["left"].set_color("#cccccc")
|
||||||
|
ax.spines["bottom"].set_color("#cccccc")
|
||||||
|
ax.set_title(title, fontsize=MIN_TITLE_FONT_SIZE, fontweight="bold", pad=8)
|
||||||
|
return fig, ax
|
||||||
|
|
||||||
|
def _save(self, fig, save_path: str) -> str:
|
||||||
|
"""Save figure with tight layout."""
|
||||||
|
Path(save_path).parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
fig.savefig(save_path, dpi=MINI_DPI, bbox_inches="tight", pad_inches=0.15)
|
||||||
|
plt.close(fig)
|
||||||
|
return save_path
|
||||||
|
|
||||||
def generate_line_trend(
|
def generate_line_trend(
|
||||||
self, data: list[dict], title: str, save_path: str
|
self,
|
||||||
) -> str:
|
years,
|
||||||
"""Generate a line trend mini-chart.
|
values,
|
||||||
|
title,
|
||||||
|
save_path,
|
||||||
|
highlight_year=None,
|
||||||
|
highlight_value=None,
|
||||||
|
color=GRAY_DARK,
|
||||||
|
secondary_color=None,
|
||||||
|
):
|
||||||
|
"""Single line chart showing trend over time."""
|
||||||
|
fig, ax = self._init_figure(title)
|
||||||
|
|
||||||
Parameters
|
ax.plot(years, values, color=color, linewidth=2, marker="o", markersize=5)
|
||||||
----------
|
|
||||||
data : list[dict]
|
|
||||||
Each dict should have 'x' and 'y' keys.
|
|
||||||
title : str
|
|
||||||
Chart title.
|
|
||||||
save_path : str
|
|
||||||
Output file path.
|
|
||||||
|
|
||||||
Returns
|
# Highlight year
|
||||||
-------
|
if highlight_year is not None:
|
||||||
str
|
idx = years.index(highlight_year) if highlight_year in years else len(years) - 1
|
||||||
Absolute path to the saved PNG file.
|
ax.axvline(
|
||||||
"""
|
x=highlight_year,
|
||||||
fig, ax = plt.subplots(figsize=MINI_FIGURE_SIZE)
|
color=BUBBLE_ZONE,
|
||||||
apply_theme(fig, ax)
|
linestyle="--",
|
||||||
|
linewidth=1,
|
||||||
|
alpha=0.7,
|
||||||
|
)
|
||||||
|
if highlight_value is not None:
|
||||||
|
ax.annotate(
|
||||||
|
str(highlight_value),
|
||||||
|
xy=(highlight_year, values[idx]),
|
||||||
|
xytext=(5, 10),
|
||||||
|
textcoords="offset points",
|
||||||
|
fontsize=MIN_ANNOTATION_FONT_SIZE,
|
||||||
|
fontweight="bold",
|
||||||
|
color=BUBBLE_ZONE,
|
||||||
|
)
|
||||||
|
|
||||||
x_values = [d["x"] for d in data]
|
|
||||||
y_values = [d["y"] for d in data]
|
|
||||||
|
|
||||||
ax.plot(x_values, y_values, color=AI_SPEND, linewidth=1.5)
|
|
||||||
ax.set_title(title, fontsize=MIN_TITLE_FONT_SIZE, fontweight="bold")
|
|
||||||
ax.tick_params(axis="both", labelsize=MIN_LABEL_FONT_SIZE)
|
ax.tick_params(axis="both", labelsize=MIN_LABEL_FONT_SIZE)
|
||||||
ax.set_xlabel("Time", fontsize=MIN_LABEL_FONT_SIZE)
|
ax.grid(True, axis="y", alpha=0.4)
|
||||||
ax.set_ylabel("Value", fontsize=MIN_LABEL_FONT_SIZE)
|
plt.tight_layout()
|
||||||
|
return self._save(fig, save_path)
|
||||||
self._save(fig, save_path)
|
|
||||||
return save_path
|
|
||||||
|
|
||||||
def generate_horizontal_bar(
|
def generate_horizontal_bar(
|
||||||
self, data: list[dict], title: str, save_path: str
|
self,
|
||||||
) -> str:
|
categories,
|
||||||
"""Generate a horizontal bar mini-chart.
|
values,
|
||||||
|
title,
|
||||||
|
save_path,
|
||||||
|
colors=None,
|
||||||
|
value_labels=None,
|
||||||
|
max_value=None,
|
||||||
|
):
|
||||||
|
"""Horizontal bar chart for comparing categories."""
|
||||||
|
fig, ax = self._init_figure(title)
|
||||||
|
|
||||||
Parameters
|
if colors is None:
|
||||||
----------
|
colors = [AI_SPEND] * len(categories)
|
||||||
data : list[dict]
|
|
||||||
Each dict should have 'label' and 'value' keys.
|
|
||||||
title : str
|
|
||||||
Chart title.
|
|
||||||
save_path : str
|
|
||||||
Output file path.
|
|
||||||
|
|
||||||
Returns
|
y_pos = range(len(categories))
|
||||||
-------
|
bars = ax.barh(
|
||||||
str
|
y_pos, values, color=colors[: len(categories)], height=0.55
|
||||||
Absolute path to the saved PNG file.
|
)
|
||||||
"""
|
|
||||||
fig, ax = plt.subplots(figsize=MINI_FIGURE_SIZE)
|
|
||||||
apply_theme(fig, ax)
|
|
||||||
|
|
||||||
labels = [d["label"] for d in data]
|
# Value labels on bars
|
||||||
values = [d["value"] for d in data]
|
if value_labels is not None:
|
||||||
colors = [
|
for bar, label in zip(bars, value_labels):
|
||||||
AI_SPEND, BUBBLE_ZONE, REVENUE, GRAY_DARK, WHITE
|
ax.text(
|
||||||
] * (len(labels) // 5 + 1)
|
bar.get_width() + max(values) * 0.01,
|
||||||
|
bar.get_y() + bar.get_height() / 2,
|
||||||
|
str(label),
|
||||||
|
va="center",
|
||||||
|
fontsize=MIN_LABEL_FONT_SIZE,
|
||||||
|
color=GRAY_DARK,
|
||||||
|
)
|
||||||
|
|
||||||
ax.barh(labels, values, color=colors[:len(labels)], height=0.6)
|
ax.set_yticks(list(y_pos))
|
||||||
ax.set_title(title, fontsize=MIN_TITLE_FONT_SIZE, fontweight="bold")
|
ax.set_yticklabels(categories, fontsize=MIN_LABEL_FONT_SIZE)
|
||||||
ax.tick_params(axis="both", labelsize=MIN_LABEL_FONT_SIZE)
|
ax.tick_params(axis="x", labelsize=MIN_LABEL_FONT_SIZE)
|
||||||
|
|
||||||
self._save(fig, save_path)
|
if max_value is not None:
|
||||||
return save_path
|
ax.set_xlim(0, max_value * 1.15)
|
||||||
|
|
||||||
|
ax.grid(True, axis="x", alpha=0.3)
|
||||||
|
plt.tight_layout()
|
||||||
|
return self._save(fig, save_path)
|
||||||
|
|
||||||
def generate_utilization_bar(
|
def generate_utilization_bar(
|
||||||
self, data: list[dict], title: str, save_path: str
|
self,
|
||||||
) -> str:
|
label,
|
||||||
"""Generate a utilization bar chart (e.g. GPU utilization).
|
percentage,
|
||||||
|
title,
|
||||||
|
save_path,
|
||||||
|
context_text=None,
|
||||||
|
):
|
||||||
|
"""Single horizontal bar showing utilization rate."""
|
||||||
|
fig, ax = self._init_figure(title)
|
||||||
|
|
||||||
Parameters
|
# Color coding based on percentage
|
||||||
----------
|
if percentage > 50:
|
||||||
data : list[dict]
|
bar_color = REVENUE # green
|
||||||
Each dict should have 'label', 'value', and 'max_value' keys.
|
elif percentage >= 20:
|
||||||
title : str
|
bar_color = WARNING_ZONE # orange
|
||||||
Chart title.
|
else:
|
||||||
save_path : str
|
bar_color = BUBBLE_ZONE # red
|
||||||
Output file path.
|
|
||||||
|
|
||||||
Returns
|
# Background track
|
||||||
-------
|
ax.barh(
|
||||||
str
|
0,
|
||||||
Absolute path to the saved PNG file.
|
100,
|
||||||
"""
|
height=0.6,
|
||||||
fig, ax = plt.subplots(figsize=MINI_FIGURE_SIZE)
|
color="#ecf0f1",
|
||||||
apply_theme(fig, ax)
|
edgecolor="#cccccc",
|
||||||
|
linewidth=0.5,
|
||||||
|
)
|
||||||
|
|
||||||
labels = [d["label"] for d in data]
|
# Filled utilization bar
|
||||||
values = [d["value"] for d in data]
|
bar = ax.barh(0, percentage, height=0.6, color=bar_color)
|
||||||
max_values = [d.get("max_value", 100) for d in data]
|
|
||||||
|
|
||||||
# Background bars for max
|
# Large percentage annotation on the bar
|
||||||
ax.barh(labels, max_values, color="#ecf0f1", height=0.6)
|
ax.text(
|
||||||
# Foreground bars for actual utilization
|
percentage / 2,
|
||||||
colors = [
|
0,
|
||||||
BUBBLE_ZONE if v / m < 0.3 else REVENUE
|
f"{percentage:.0f}%",
|
||||||
for v, m in zip(values, max_values)
|
ha="center",
|
||||||
]
|
va="center",
|
||||||
ax.barh(labels, values, color=colors, height=0.6)
|
fontsize=MIN_ANNOTATION_FONT_SIZE + 3,
|
||||||
|
fontweight="bold",
|
||||||
|
color=WHITE if percentage > 10 else GRAY_DARK,
|
||||||
|
)
|
||||||
|
|
||||||
ax.set_title(title, fontsize=MIN_TITLE_FONT_SIZE, fontweight="bold")
|
# Label
|
||||||
ax.tick_params(axis="both", labelsize=MIN_LABEL_FONT_SIZE)
|
ax.set_yticks([])
|
||||||
|
ax.set_xlim(0, 100)
|
||||||
|
ax.text(
|
||||||
|
0.02,
|
||||||
|
-0.25,
|
||||||
|
str(label),
|
||||||
|
transform=ax.transData,
|
||||||
|
fontsize=MIN_LABEL_FONT_SIZE,
|
||||||
|
color=GRAY_DARK,
|
||||||
|
)
|
||||||
|
|
||||||
self._save(fig, save_path)
|
# Context text below the bar
|
||||||
return save_path
|
if context_text is not None:
|
||||||
|
ax.text(
|
||||||
|
50,
|
||||||
|
-0.55,
|
||||||
|
context_text,
|
||||||
|
ha="center",
|
||||||
|
fontsize=MIN_LABEL_FONT_SIZE - 1,
|
||||||
|
color=GRAY_MEDIUM,
|
||||||
|
style="italic",
|
||||||
|
)
|
||||||
|
|
||||||
|
ax.set_ylim(-0.9, 0.5)
|
||||||
|
ax.axis("off")
|
||||||
|
plt.tight_layout()
|
||||||
|
return self._save(fig, save_path)
|
||||||
|
|
||||||
def generate_comparison_bar(
|
def generate_comparison_bar(
|
||||||
self, data: list[dict], title: str, save_path: str
|
self,
|
||||||
) -> str:
|
categories,
|
||||||
"""Generate a grouped comparison bar chart.
|
values_left,
|
||||||
|
values_right,
|
||||||
|
title,
|
||||||
|
save_path,
|
||||||
|
label_left=None,
|
||||||
|
label_right=None,
|
||||||
|
colors=None,
|
||||||
|
):
|
||||||
|
"""Side-by-side grouped bar chart for comparisons."""
|
||||||
|
fig, ax = self._init_figure(title)
|
||||||
|
|
||||||
Parameters
|
if colors is None:
|
||||||
----------
|
color_left = AI_SPEND
|
||||||
data : list[dict]
|
color_right = GRAY_MEDIUM
|
||||||
Each dict should have 'label' and multiple value keys
|
elif len(colors) >= 2:
|
||||||
(e.g. 'value_a', 'value_b').
|
color_left = colors[0]
|
||||||
title : str
|
color_right = colors[1]
|
||||||
Chart title.
|
else:
|
||||||
save_path : str
|
color_left = colors[0] if len(colors) == 1 else AI_SPEND
|
||||||
Output file path.
|
color_right = GRAY_MEDIUM
|
||||||
|
|
||||||
Returns
|
x = list(range(len(categories)))
|
||||||
-------
|
|
||||||
str
|
|
||||||
Absolute path to the saved PNG file.
|
|
||||||
"""
|
|
||||||
fig, ax = plt.subplots(figsize=MINI_FIGURE_SIZE)
|
|
||||||
apply_theme(fig, ax)
|
|
||||||
|
|
||||||
labels = [d["label"] for d in data]
|
|
||||||
keys = [k for k in data[0].keys() if k != "label"]
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
x = np.arange(len(labels))
|
|
||||||
width = 0.35
|
width = 0.35
|
||||||
|
|
||||||
for i, key in enumerate(keys):
|
bars_left = ax.bar(
|
||||||
values = [d.get(key, 0) for d in data]
|
[p - width / 2 for p in x],
|
||||||
color = AI_SPEND if i == 0 else REVENUE
|
values_left,
|
||||||
ax.bar(x + i * width, values, width, label=key, color=color)
|
width,
|
||||||
|
label=label_left or "Left",
|
||||||
|
color=color_left,
|
||||||
|
)
|
||||||
|
bars_right = ax.bar(
|
||||||
|
[p + width / 2 for p in x],
|
||||||
|
values_right,
|
||||||
|
width,
|
||||||
|
label=label_right or "Right",
|
||||||
|
color=color_right,
|
||||||
|
)
|
||||||
|
|
||||||
ax.set_title(title, fontsize=MIN_TITLE_FONT_SIZE, fontweight="bold")
|
ax.set_xticks(x)
|
||||||
ax.set_xticks(x + width / 2)
|
ax.set_xticklabels(categories, fontsize=MIN_LABEL_FONT_SIZE)
|
||||||
ax.set_xticklabels(labels, fontsize=MIN_LABEL_FONT_SIZE)
|
|
||||||
ax.tick_params(axis="y", labelsize=MIN_LABEL_FONT_SIZE)
|
ax.tick_params(axis="y", labelsize=MIN_LABEL_FONT_SIZE)
|
||||||
ax.legend(fontsize=MIN_LABEL_FONT_SIZE)
|
ax.legend(
|
||||||
|
loc="upper center",
|
||||||
|
bbox_to_anchor=(0.5, -0.12),
|
||||||
|
ncol=2,
|
||||||
|
fontsize=MIN_LABEL_FONT_SIZE,
|
||||||
|
)
|
||||||
|
ax.grid(True, axis="y", alpha=0.3)
|
||||||
|
plt.tight_layout()
|
||||||
|
return self._save(fig, save_path)
|
||||||
|
|
||||||
self._save(fig, save_path)
|
|
||||||
return save_path
|
|
||||||
|
|
||||||
@staticmethod
|
# ---------------------------------------------------------------------------
|
||||||
def _save(fig: matplotlib.figure.Figure, save_path: str) -> None:
|
# Standalone convenience functions (for use by card workers)
|
||||||
"""Save a figure with tight layout at MINI_DPI."""
|
# ---------------------------------------------------------------------------
|
||||||
Path(save_path).parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
fig.savefig(save_path, dpi=MINI_DPI, bbox_inches="tight")
|
def create_cape_chart(years, values, save_path):
|
||||||
plt.close(fig)
|
"""Create historical CAPE trend with current value highlighted."""
|
||||||
|
engine = MiniChartEngine()
|
||||||
|
engine.generate_line_trend(
|
||||||
|
years=years,
|
||||||
|
values=values,
|
||||||
|
title="CAPE Ratio Trend",
|
||||||
|
save_path=save_path,
|
||||||
|
highlight_year=years[-1],
|
||||||
|
highlight_value=values[-1],
|
||||||
|
color=GRAY_DARK,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_capex_chart(companies, values, save_path):
|
||||||
|
"""Create hyperscaler capex comparison bar chart."""
|
||||||
|
engine = MiniChartEngine()
|
||||||
|
colors_list = [AI_SPEND, WARNING_ZONE, REVENUE, GRAY_MEDIUM, BUBBLE_ZONE]
|
||||||
|
engine.generate_horizontal_bar(
|
||||||
|
categories=companies,
|
||||||
|
values=values,
|
||||||
|
title="Hyperscaler AI Capex",
|
||||||
|
save_path=save_path,
|
||||||
|
colors=colors_list[: len(companies)],
|
||||||
|
value_labels=[f"${v}B" for v in values],
|
||||||
|
max_value=max(values) * 1.3,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_utilization_chart(percentage, context_text, save_path):
|
||||||
|
"""Create GPU utilization gauge chart."""
|
||||||
|
engine = MiniChartEngine()
|
||||||
|
engine.generate_utilization_bar(
|
||||||
|
label="GPU Utilization",
|
||||||
|
percentage=percentage,
|
||||||
|
title="Current GPU Utilization",
|
||||||
|
save_path=save_path,
|
||||||
|
context_text=context_text,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_vulnerability_chart(ai_rate, non_ai_rate, save_path):
|
||||||
|
"""Create AI vs non-AI code vulnerability comparison."""
|
||||||
|
engine = MiniChartEngine()
|
||||||
|
engine.generate_comparison_bar(
|
||||||
|
categories=["Code Vulnerability Rate"],
|
||||||
|
values_left=[ai_rate],
|
||||||
|
values_right=[non_ai_rate],
|
||||||
|
title="AI vs Non-AI Code Vulnerability",
|
||||||
|
save_path=save_path,
|
||||||
|
label_left="AI-Generated Code",
|
||||||
|
label_right="Human-Generated Code",
|
||||||
|
colors=[BUBBLE_ZONE, GRAY_MEDIUM],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -213,61 +340,64 @@ class MiniChartEngine:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def _run_tests() -> None:
|
def _run_tests() -> None:
|
||||||
"""Generate 4 placeholder test charts to output/battlecards/charts/."""
|
"""Generate 4 test charts demonstrating each chart type."""
|
||||||
base_dir = Path("output/battlecards/charts")
|
base_dir = Path("output/battlecards/charts")
|
||||||
engine = MiniChartEngine()
|
engine = MiniChartEngine()
|
||||||
|
base_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# Test 1: Line trend
|
# Test 1: Line trend — CAPE-like trend with 2026 highlighted
|
||||||
line_data = [
|
years = [2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025, 2026]
|
||||||
{"x": f"Q{i}", "y": 10 + i * 3} for i in range(1, 5)
|
cape_values = [18.2, 28.5, 31.1, 25.3, 22.7, 33.8, 37.2, 41.5, 44.8]
|
||||||
]
|
|
||||||
path1 = engine.generate_line_trend(
|
path1 = engine.generate_line_trend(
|
||||||
line_data,
|
years=years,
|
||||||
"AI Spending Trend (Test)",
|
values=cape_values,
|
||||||
str(base_dir / "test_line_trend.png"),
|
title="S&P 500 CAPE Ratio Trend",
|
||||||
|
save_path=str(base_dir / "test_line_trend.png"),
|
||||||
|
highlight_year=2026,
|
||||||
|
highlight_value="44.8x",
|
||||||
|
color=GRAY_DARK,
|
||||||
)
|
)
|
||||||
print(f" Line trend: {path1}")
|
print(f" [1/4] Line trend: {path1}")
|
||||||
|
|
||||||
# Test 2: Horizontal bar
|
# Test 2: Horizontal bar — Hyperscaler capex comparison
|
||||||
bar_data = [
|
companies = ["Microsoft", "Amazon", "Google", "Meta"]
|
||||||
{"label": "Cloud A", "value": 85},
|
capex_values = [234, 118, 90, 72]
|
||||||
{"label": "Cloud B", "value": 62},
|
capex_colors = ["#00a4ef", "#ff9900", "#4285f4", "#1877f2"]
|
||||||
{"label": "On-prem", "value": 28},
|
|
||||||
]
|
|
||||||
path2 = engine.generate_horizontal_bar(
|
path2 = engine.generate_horizontal_bar(
|
||||||
bar_data,
|
categories=companies,
|
||||||
"AI Infrastructure Spending (Test)",
|
values=capex_values,
|
||||||
str(base_dir / "test_horizontal_bar.png"),
|
title="2025 AI Infrastructure Capex ($B)",
|
||||||
|
save_path=str(base_dir / "test_horizontal_bar.png"),
|
||||||
|
colors=capex_colors,
|
||||||
|
value_labels=[f"${v}B" for v in capex_values],
|
||||||
|
max_value=280,
|
||||||
)
|
)
|
||||||
print(f" Horizontal bar: {path2}")
|
print(f" [2/4] Horizontal bar: {path2}")
|
||||||
|
|
||||||
# Test 3: Utilization bar
|
# Test 3: Utilization bar — GPU utilization gauge at 5%
|
||||||
util_data = [
|
|
||||||
{"label": "Company A", "value": 18, "max_value": 100},
|
|
||||||
{"label": "Company B", "value": 24, "max_value": 100},
|
|
||||||
{"label": "Company C", "value": 45, "max_value": 100},
|
|
||||||
]
|
|
||||||
path3 = engine.generate_utilization_bar(
|
path3 = engine.generate_utilization_bar(
|
||||||
util_data,
|
label="Enterprise Average",
|
||||||
"GPU Utilization (Test)",
|
percentage=5.0,
|
||||||
str(base_dir / "test_utilization_bar.png"),
|
title="GPU Utilization Rate",
|
||||||
|
save_path=str(base_dir / "test_utilization_bar.png"),
|
||||||
|
context_text="Most GPUs sit idle — 95% capacity wasted",
|
||||||
)
|
)
|
||||||
print(f" Utilization bar: {path3}")
|
print(f" [3/4] Utilization bar: {path3}")
|
||||||
|
|
||||||
# Test 4: Comparison bar
|
# Test 4: Comparison bar — AI vs non-AI vulnerability rates
|
||||||
comp_data = [
|
|
||||||
{"label": "2023", "value_a": 50, "value_b": 30},
|
|
||||||
{"label": "2024", "value_a": 75, "value_b": 45},
|
|
||||||
{"label": "2025", "value_a": 90, "value_b": 55},
|
|
||||||
]
|
|
||||||
path4 = engine.generate_comparison_bar(
|
path4 = engine.generate_comparison_bar(
|
||||||
comp_data,
|
categories=["Vulnerability Rate (%)"],
|
||||||
"Spend vs Revenue Comparison (Test)",
|
values_left=[47],
|
||||||
str(base_dir / "test_comparison_bar.png"),
|
values_right=[12],
|
||||||
|
title="Code Vulnerability Comparison",
|
||||||
|
save_path=str(base_dir / "test_comparison_bar.png"),
|
||||||
|
label_left="AI-Generated Code",
|
||||||
|
label_right="Human-Generated Code",
|
||||||
|
colors=[BUBBLE_ZONE, GRAY_MEDIUM],
|
||||||
)
|
)
|
||||||
print(f" Comparison bar: {path4}")
|
print(f" [4/4] Comparison bar: {path4}")
|
||||||
|
|
||||||
print("All 4 test charts generated successfully.")
|
print("\nAll 4 test charts generated successfully.")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
@@ -277,7 +407,7 @@ if __name__ == "__main__":
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--test",
|
"--test",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Generate 4 placeholder test charts",
|
help="Generate 4 test charts demonstrating each chart type",
|
||||||
)
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
|||||||