if (btn) {
btn.innerHTML = next === 'dark' ? '☼' : '☾'; // sun or moon
}
+ applyLineColors();
// Rebuild scroll marker with new theme colors
buildScrollMarker();
return parseInt(text) || 0;
}
-function getIntensityClass(ratio) {
- if (ratio > 0.75) return 'vhot';
- if (ratio > 0.5) return 'hot';
- if (ratio > 0.25) return 'warm';
- return 'cold';
-}
-
// ============================================================================
// Scroll Minimap
// ============================================================================
const lineTop = Math.floor(line.offsetTop * markerScale);
const lineNumber = index + 1;
- const intensityClass = maxSamples > 0 ? getIntensityClass(samples / maxSamples) : 'cold';
+ const intensityClass = maxSamples > 0 ? (intensityToClass(samples / maxSamples) || 'cold') : 'cold';
if (lineNumber === prevLine + 1 && lastMark?.classList.contains(intensityClass)) {
lastMark.style.height = `${lineTop + lineHeight - lastTop}px`;
document.body.appendChild(scrollMarker);
}
+function applyLineColors() {
+ const lines = document.querySelectorAll('.code-line');
+ lines.forEach(line => {
+ let intensity;
+ if (colorMode === 'self') {
+ intensity = parseFloat(line.getAttribute('data-self-intensity')) || 0;
+ } else {
+ intensity = parseFloat(line.getAttribute('data-cumulative-intensity')) || 0;
+ }
+
+ const color = intensityToColor(intensity);
+ line.style.background = color;
+ });
+}
+
// ============================================================================
// Toggle Controls
// ============================================================================
function toggleColorMode() {
colorMode = colorMode === 'self' ? 'cumulative' : 'self';
- const lines = document.querySelectorAll('.code-line');
-
- lines.forEach(line => {
- let bgColor;
- if (colorMode === 'self') {
- bgColor = line.getAttribute('data-self-color');
- } else {
- bgColor = line.getAttribute('data-cumulative-color');
- }
-
- if (bgColor) {
- line.style.background = bgColor;
- }
- });
+ applyLineColors();
updateToggleUI('toggle-color-mode', colorMode === 'cumulative');
document.addEventListener('DOMContentLoaded', function() {
// Restore UI state (theme, etc.)
restoreUIState();
-
- // Apply background colors
- document.querySelectorAll('.code-line[data-bg-color]').forEach(line => {
- const bgColor = line.getAttribute('data-bg-color');
- if (bgColor) {
- line.style.background = bgColor;
- }
- });
+ applyLineColors();
// Initialize navigation buttons
document.querySelectorAll('.nav-btn').forEach(button => {
import html
import importlib.resources
import json
+import math
import os
import platform
import site
children: Dict[str, 'TreeNode'] = field(default_factory=dict)
-@dataclass
-class ColorGradient:
- """Configuration for heatmap color gradient calculations."""
- # Color stops thresholds
- stop_1: float = 0.2 # Blue to cyan transition
- stop_2: float = 0.4 # Cyan to green transition
- stop_3: float = 0.6 # Green to yellow transition
- stop_4: float = 0.8 # Yellow to orange transition
- stop_5: float = 1.0 # Orange to red transition
-
- # Alpha (opacity) values
- alpha_very_cold: float = 0.3
- alpha_cold: float = 0.4
- alpha_medium: float = 0.5
- alpha_warm: float = 0.6
- alpha_hot_base: float = 0.7
- alpha_hot_range: float = 0.15
-
- # Gradient multiplier
- multiplier: int = 5
-
- # Cache for calculated colors
- cache: Dict[float, Tuple[int, int, int, float]] = field(default_factory=dict)
-
-
# ============================================================================
# Module Path Analysis
# ============================================================================
self.file_css = css_content
# Load JS
- self.index_js = (assets_dir / "heatmap_index.js").read_text(encoding="utf-8")
- self.file_js = (assets_dir / "heatmap.js").read_text(encoding="utf-8")
+ shared_js = (assets_dir / "heatmap_shared.js").read_text(encoding="utf-8")
+ self.index_js = f"{shared_js}\n{(assets_dir / 'heatmap_index.js').read_text(encoding='utf-8')}"
+ self.file_js = f"{shared_js}\n{(assets_dir / 'heatmap.js').read_text(encoding='utf-8')}"
# Load Python logo
logo_dir = template_dir / "_assets"
class _HtmlRenderer:
"""Renders hierarchical tree structures as HTML."""
- def __init__(self, file_index: Dict[str, str], color_gradient: ColorGradient,
- calculate_intensity_color_func):
- """Initialize renderer with file index and color calculation function.
+ def __init__(self, file_index: Dict[str, str]):
+ """Initialize renderer with file index.
Args:
file_index: Mapping from filenames to HTML file names
- color_gradient: ColorGradient configuration
- calculate_intensity_color_func: Function to calculate colors
"""
self.file_index = file_index
- self.color_gradient = color_gradient
- self.calculate_intensity_color = calculate_intensity_color_func
self.heatmap_bar_height = 16
def render_hierarchical_html(self, trees: Dict[str, TreeNode]) -> str:
module_name = html.escape(stat.module_name)
intensity = stat.percentage / 100.0
- r, g, b, alpha = self.calculate_intensity_color(intensity)
- bg_color = f"rgba({r}, {g}, {b}, {alpha})"
bar_width = min(stat.percentage, 100)
html_file = self.file_index[stat.filename]
return (f'{indent}<div class="file-item">\n'
f'{indent} <a href="{html_file}" class="file-link" title="{full_path}">📄 {module_name}</a>\n'
f'{indent} <span class="file-samples">{stat.total_samples:,} samples</span>\n'
- f'{indent} <div class="heatmap-bar-container"><div class="heatmap-bar" style="width: {bar_width}px; background-color: {bg_color}; height: {self.heatmap_bar_height}px;"></div></div>\n'
+ f'{indent} <div class="heatmap-bar-container"><div class="heatmap-bar" style="width: {bar_width}px; height: {self.heatmap_bar_height}px;" data-intensity="{intensity:.3f}"></div></div>\n'
f'{indent}</div>\n')
self._path_info = get_python_path_info()
self.stats = {}
- # Color gradient configuration
- self._color_gradient = ColorGradient()
-
# Template loader (loads all templates once)
self._template_loader = _TemplateLoader()
# File index (populated during export)
self.file_index = {}
- @property
- def _color_cache(self):
- """Compatibility property for accessing color cache."""
- return self._color_gradient.cache
-
def set_stats(self, sample_interval_usec, duration_sec, sample_rate, error_rate=None, missed_samples=None, **kwargs):
"""Set profiling statistics to include in heatmap output.
tree = _TreeBuilder.build_file_tree(file_stats)
# Render tree as HTML
- renderer = _HtmlRenderer(self.file_index, self._color_gradient,
- self._calculate_intensity_color)
+ renderer = _HtmlRenderer(self.file_index)
sections_html = renderer.render_hierarchical_html(tree)
# Format error rate and missed samples with bar classes
except (IOError, OSError) as e:
raise RuntimeError(f"Failed to write index file {index_path}: {e}") from e
- def _calculate_intensity_color(self, intensity: float) -> Tuple[int, int, int, float]:
- """Calculate RGB color and alpha for given intensity (0-1 range).
-
- Returns (r, g, b, alpha) tuple representing the heatmap color gradient:
- blue -> green -> yellow -> orange -> red
-
- Results are cached to improve performance.
- """
- # Round to 3 decimal places for cache key
- cache_key = round(intensity, 3)
- if cache_key in self._color_gradient.cache:
- return self._color_gradient.cache[cache_key]
-
- gradient = self._color_gradient
- m = gradient.multiplier
-
- # Color stops with (threshold, rgb_func, alpha_func)
- stops = [
- (gradient.stop_1,
- lambda i: (0, int(150 * i * m), 255),
- lambda i: gradient.alpha_very_cold),
- (gradient.stop_2,
- lambda i: (0, 255, int(255 * (1 - (i - gradient.stop_1) * m))),
- lambda i: gradient.alpha_cold),
- (gradient.stop_3,
- lambda i: (int(255 * (i - gradient.stop_2) * m), 255, 0),
- lambda i: gradient.alpha_medium),
- (gradient.stop_4,
- lambda i: (255, int(200 - 100 * (i - gradient.stop_3) * m), 0),
- lambda i: gradient.alpha_warm),
- (gradient.stop_5,
- lambda i: (255, int(100 * (1 - (i - gradient.stop_4) * m)), 0),
- lambda i: gradient.alpha_hot_base + gradient.alpha_hot_range * (i - gradient.stop_4) * m),
- ]
-
- result = None
- for threshold, rgb_func, alpha_func in stops:
- if intensity < threshold or threshold == gradient.stop_5:
- r, g, b = rgb_func(intensity)
- result = (r, g, b, alpha_func(intensity))
- break
-
- # Fallback
- if result is None:
- result = (255, 0, 0, 0.75)
-
- # Cache the result
- self._color_gradient.cache[cache_key] = result
- return result
-
def _generate_file_html(self, output_path: Path, filename: str,
line_counts: Dict[int, int], self_counts: Dict[int, int],
file_stat: FileStats):
# Calculate colors for both self and cumulative modes
if cumulative_samples > 0:
- cumulative_intensity = cumulative_samples / max_samples if max_samples > 0 else 0
- self_intensity = self_samples / max_self_samples if max_self_samples > 0 and self_samples > 0 else 0
-
- # Default to self-based coloring
- intensity = self_intensity if self_samples > 0 else cumulative_intensity
- r, g, b, alpha = self._calculate_intensity_color(intensity)
- bg_color = f"rgba({r}, {g}, {b}, {alpha})"
-
- # Pre-calculate colors for both modes (for JS toggle)
- self_bg_color = self._format_color_for_intensity(self_intensity) if self_samples > 0 else "transparent"
- cumulative_bg_color = self._format_color_for_intensity(cumulative_intensity)
+ log_cumulative = math.log(cumulative_samples + 1)
+ log_max = math.log(max_samples + 1)
+ cumulative_intensity = log_cumulative / log_max if log_max > 0 else 0
+
+ if self_samples > 0 and max_self_samples > 0:
+ log_self = math.log(self_samples + 1)
+ log_max_self = math.log(max_self_samples + 1)
+ self_intensity = log_self / log_max_self if log_max_self > 0 else 0
+ else:
+ self_intensity = 0
self_display = f"{self_samples:,}" if self_samples > 0 else ""
cumulative_display = f"{cumulative_samples:,}"
tooltip = f"Self: {self_samples:,}, Total: {cumulative_samples:,}"
else:
- bg_color = "transparent"
- self_bg_color = "transparent"
- cumulative_bg_color = "transparent"
+ cumulative_intensity = 0
+ self_intensity = 0
self_display = ""
cumulative_display = ""
tooltip = ""
# Get navigation buttons
nav_buttons_html = self._build_navigation_buttons(filename, line_num)
- # Build line HTML
+ # Build line HTML with intensity data attributes
line_html = html.escape(line_content.rstrip('\n'))
title_attr = f' title="{html.escape(tooltip)}"' if tooltip else ""
return (
- f' <div class="code-line" data-bg-color="{bg_color}" '
- f'data-self-color="{self_bg_color}" data-cumulative-color="{cumulative_bg_color}" '
+ f' <div class="code-line" '
+ f'data-self-intensity="{self_intensity:.3f}" '
+ f'data-cumulative-intensity="{cumulative_intensity:.3f}" '
f'id="line-{line_num}"{title_attr}>\n'
f' <div class="line-number">{line_num}</div>\n'
f' <div class="line-samples-self">{self_display}</div>\n'
f' </div>\n'
)
- def _format_color_for_intensity(self, intensity: float) -> str:
- """Format color as rgba() string for given intensity."""
- r, g, b, alpha = self._calculate_intensity_color(intensity)
- return f"rgba({r}, {g}, {b}, {alpha})"
-
def _build_navigation_buttons(self, filename: str, line_num: int) -> str:
"""Build navigation buttons for callers/callees."""
line_key = (filename, line_num)