import pandas as pd import numpy as np import matplotlib.pyplot as plt from datetime import datetime import matplotlib.dates as mdates from matplotlib.patches import Circle, RegularPolygon from matplotlib.path import Path from matplotlib.projections.polar import PolarAxes from matplotlib.projections import register_projection from matplotlib.spines import Spine from matplotlib.transforms import Affine2D import locale import matplotlib.ticker as mtick # Set locale for number formatting try: locale.setlocale(locale.LC_ALL, '') except: pass # Fallback if locale setting fails def format_number(number, precision=2, currency_symbol='$', format_type='auto'): """ Format large numbers with K, M, B, T suffixes or with commas Parameters: - number: The number to format - precision: Decimal precision - currency_symbol: Currency symbol to use - format_type: 'auto', 'suffix', 'comma', 'millions', 'billions' """ if number is None or number == 'N/A': return 'N/A' try: number = float(number) except: return str(number) # Handle negative numbers is_negative = number < 0 abs_number = abs(number) # Format based on type if format_type == 'comma': # Format with commas try: formatted = locale.format_string(f"%.{precision}f", abs_number, grouping=True) except: # Fallback if locale formatting fails formatted = f"{abs_number:,.{precision}f}" elif format_type == 'millions': # Always format in millions formatted = f"{abs_number / 1_000_000:.{precision}f}M" elif format_type == 'billions': # Always format in billions formatted = f"{abs_number / 1_000_000_000:.{precision}f}B" else: # 'auto' or 'suffix' # Format with appropriate suffix based on magnitude if abs_number >= 1_000_000_000_000: formatted = f"{abs_number / 1_000_000_000_000:.{precision}f}T" elif abs_number >= 1_000_000_000: formatted = f"{abs_number / 1_000_000_000:.{precision}f}B" elif abs_number >= 1_000_000: formatted = f"{abs_number / 1_000_000:.{precision}f}M" elif abs_number >= 1_000: formatted = f"{abs_number / 1_000:.{precision}f}K" else: formatted = f"{abs_number:.{precision}f}" # Add negative sign if needed if is_negative: return f"-{currency_symbol}{formatted}" else: return f"{currency_symbol}{formatted}" def format_percentage(number, precision=2): """Format number as percentage""" if number is None or number == 'N/A': return 'N/A' try: number = float(number) return f"{number:.{precision}f}%" except: return str(number) def create_price_chart(price_history): """Create a price chart from historical data""" # Close any existing figures to prevent memory issues plt.close('all') if price_history is None or len(price_history) == 0: fig = plt.figure(figsize=(10, 6)) plt.text(0.5, 0.5, "No price history data available", horizontalalignment='center', verticalalignment='center', fontsize=14) plt.axis('off') return fig fig = plt.figure(figsize=(10, 6)) # Calculate moving averages if enough data points if len(price_history) > 50: ma50 = price_history.rolling(window=50).mean() ma200 = price_history.rolling(window=min(200, len(price_history))).mean() plt.plot(price_history.index, price_history.values, label='Price') plt.plot(ma50.index, ma50.values, label='50-Day MA', linestyle='--') plt.plot(ma200.index, ma200.values, label='200-Day MA', linestyle='-.') plt.legend() else: plt.plot(price_history.index, price_history.values) # Format x-axis to show dates nicely plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%b %Y')) plt.gca().xaxis.set_major_locator(mdates.MonthLocator(interval=2)) plt.gcf().autofmt_xdate() # Add grid and labels plt.title('Stock Price History', fontsize=14) plt.xlabel('Date') plt.ylabel('Price') plt.grid(True, alpha=0.3) plt.tight_layout() return fig def format_metrics_table(metrics): """Format metrics for display in a table""" formatted_metrics = {} currency_symbol = metrics.get('Currency Symbol', '$') for key, value in metrics.items(): if key == 'Market Cap': formatted_metrics[key] = format_number(value, currency_symbol=currency_symbol) elif key in ['Dividend Yield (%)', 'Profit Margin', 'Operating Margin', 'ROE', 'ROA', 'Revenue Growth', 'Payout Ratio', 'Earnings Growth', 'EBITDA Margins', 'Gross Margins'] or key.endswith('(%)'): formatted_metrics[key] = format_percentage(value) elif key in ['Current Price', 'EPS', '52 Week High', '52 Week Low', '50-Day MA', '200-Day MA', 'Revenue Per Share', 'Target Mean Price', 'Free Cash Flow', 'Operating Cash Flow']: if value != 'N/A': formatted_metrics[key] = f"{currency_symbol}{value:.2f}" else: formatted_metrics[key] = value elif key in ['P/E Ratio', 'P/B Ratio', 'Forward P/E', 'PEG Ratio', 'Debt to Equity', 'Current Ratio', 'Quick Ratio', 'Beta', 'EV/EBITDA', 'EV/Revenue']: if value != 'N/A': formatted_metrics[key] = f"{value:.2f}" else: formatted_metrics[key] = value else: formatted_metrics[key] = value return formatted_metrics def format_financial_statement(statement, statement_type, currency_symbol='$', format_type='millions'): """ Format financial statement for display Parameters: - statement: The financial statement DataFrame - statement_type: Type of statement (for title) - currency_symbol: Currency symbol to use - format_type: How to format numbers ('comma', 'millions', 'billions', 'auto') """ if statement is None or statement.empty: return pd.DataFrame() # Make a copy to avoid modifying the original df = statement.copy() # Format column names (dates) df.columns = [col.strftime('%Y-%m-%d') if isinstance(col, datetime) else str(col) for col in df.columns] # Format the values based on format_type if format_type == 'comma': # Format with commas for col in df.columns: df[col] = df[col].apply(lambda x: format_number(x, currency_symbol=currency_symbol, format_type='comma') if pd.notnull(x) else 'N/A') elif format_type == 'millions': # Convert to millions and format df = df / 1_000_000 for col in df.columns: df[col] = df[col].apply(lambda x: f"{currency_symbol}{x:.2f}M" if pd.notnull(x) else 'N/A') elif format_type == 'billions': # Convert to billions and format df = df / 1_000_000_000 for col in df.columns: df[col] = df[col].apply(lambda x: f"{currency_symbol}{x:.2f}B" if pd.notnull(x) else 'N/A') else: # 'auto' # Determine appropriate scale based on data magnitude max_abs_val = abs(df.max().max()) if max_abs_val >= 1_000_000_000: df = df / 1_000_000_000 suffix = 'B' else: df = df / 1_000_000 suffix = 'M' for col in df.columns: df[col] = df[col].apply(lambda x: f"{currency_symbol}{x:.2f}{suffix}" if pd.notnull(x) else 'N/A') return df def prepare_financial_table(statement, currency_symbol='$', format_type='millions'): """ Prepare financial statement for display in a table format Returns a dictionary with formatted data and metadata """ if statement is None or statement.empty: return {"error": "No data available"} # Format the statement formatted_df = format_financial_statement(statement, "", currency_symbol, format_type) # Prepare data for display result = { "data": formatted_df.reset_index().to_dict('records'), "columns": [{"name": "Metric", "id": "index"}] + [{"name": col, "id": col} for col in formatted_df.columns], "format_type": format_type, "currency_symbol": currency_symbol } return result def create_financial_chart(statement, title, chart_type='bar'): """Create a chart from financial statement data""" # Close any existing figures to prevent memory issues plt.close('all') if statement is None or statement.empty: fig = plt.figure(figsize=(12, 6)) plt.text(0.5, 0.5, "No data available", horizontalalignment='center', verticalalignment='center', fontsize=14) plt.axis('off') return fig # Select key metrics based on statement type if 'Total Revenue' in statement.index: # Income Statement metrics = ['Total Revenue', 'Gross Profit', 'Operating Income', 'Net Income'] elif 'Total Assets' in statement.index: # Balance Sheet metrics = ['Total Assets', 'Total Liabilities Net Minority Interest', 'Total Equity Gross Minority Interest'] elif 'Operating Cash Flow' in statement.index: # Cash Flow metrics = ['Operating Cash Flow', 'Free Cash Flow', 'Capital Expenditures'] else: # Default to first 4 rows if specific metrics not found metrics = statement.index[:4] # Filter for selected metrics that exist in the statement metrics = [m for m in metrics if m in statement.index] if not metrics: fig = plt.figure(figsize=(12, 6)) plt.text(0.5, 0.5, "No relevant metrics found", horizontalalignment='center', verticalalignment='center', fontsize=14) plt.axis('off') return fig # Get data for the selected metrics data = statement.loc[metrics] # Convert to millions for better readability data = data / 1_000_000 # Create the chart fig = plt.figure(figsize=(12, 6)) if chart_type == 'bar': ax = data.T.plot(kind='bar', ax=plt.gca(), width=0.8) # Add value labels on top of bars for container in ax.containers: ax.bar_label(container, fmt='%.1fM', fontsize=8) else: # line chart ax = data.T.plot(kind='line', marker='o', ax=plt.gca()) # Add value labels at data points for line, metric in zip(ax.get_lines(), metrics): x_data, y_data = line.get_data() for x, y in zip(x_data, y_data): ax.annotate(f'{y:.1f}M', (x, y), textcoords="offset points", xytext=(0,5), ha='center', fontsize=8) plt.title(title, fontsize=14) plt.ylabel('Millions ($)') plt.grid(True, alpha=0.3) plt.legend(loc='best') plt.tight_layout() return fig def create_key_metrics_chart(statement, title, metrics_list, currency_symbol='$'): """Create a chart for specific key metrics from financial statements""" # Close any existing figures to prevent memory issues plt.close('all') if statement is None or statement.empty: fig = plt.figure(figsize=(12, 6)) plt.text(0.5, 0.5, "No data available", horizontalalignment='center', verticalalignment='center', fontsize=14) plt.axis('off') return fig # Filter for selected metrics that exist in the statement available_metrics = [m for m in metrics_list if m in statement.index] if not available_metrics: fig = plt.figure(figsize=(12, 6)) plt.text(0.5, 0.5, "No relevant metrics found", horizontalalignment='center', verticalalignment='center', fontsize=14) plt.axis('off') return fig # Get data for the selected metrics data = statement.loc[available_metrics] # Convert to millions for better readability data = data / 1_000_000 # Create the chart fig = plt.figure(figsize=(12, 6)) # Create a bar chart ax = data.T.plot(kind='bar', ax=plt.gca(), width=0.8) # Add value labels on top of bars for container in ax.containers: ax.bar_label(container, fmt=f'%.1fM', fontsize=8) plt.title(title, fontsize=14) plt.ylabel(f'Millions ({currency_symbol})') plt.grid(True, alpha=0.3) plt.legend(loc='best') plt.tight_layout() return fig def create_growth_chart(statement, metric_name, title): """Create a growth rate chart for a specific metric""" # Close any existing figures to prevent memory issues plt.close('all') if statement is None or statement.empty or metric_name not in statement.index: fig = plt.figure(figsize=(10, 6)) plt.text(0.5, 0.5, f"No data available for {metric_name}", horizontalalignment='center', verticalalignment='center', fontsize=14) plt.axis('off') return fig # Get data for the selected metric data = statement.loc[metric_name] # Calculate year-over-year growth rates growth_rates = data.pct_change(-1) * 100 # Multiply by -1 to get YoY since columns are in reverse chronological order # Create the chart fig = plt.figure(figsize=(10, 6)) # Plot the growth rates ax = plt.gca() bars = ax.bar(growth_rates.index, growth_rates.values, color='teal', alpha=0.7) # Add value labels on top of bars for bar in bars: height = bar.get_height() if not np.isnan(height): ax.text(bar.get_x() + bar.get_width()/2., height + (1 if height >= 0 else -5), f'{height:.1f}%', ha='center', va='bottom' if height >= 0 else 'top', fontsize=9) # Format y-axis as percentage ax.yaxis.set_major_formatter(mtick.PercentFormatter()) # Add a horizontal line at y=0 plt.axhline(y=0, color='black', linestyle='-', alpha=0.3) plt.title(title, fontsize=14) plt.ylabel('Year-over-Year Growth (%)') plt.grid(True, alpha=0.3) plt.tight_layout() return fig def create_margin_chart(statement, title): """Create a chart showing margin trends""" # Close any existing figures to prevent memory issues plt.close('all') # Check if we have the necessary data required_metrics = ['Total Revenue', 'Gross Profit', 'Operating Income', 'Net Income'] if statement is None or statement.empty or not all(metric in statement.index for metric in required_metrics): fig = plt.figure(figsize=(10, 6)) plt.text(0.5, 0.5, "Insufficient data for margin analysis", horizontalalignment='center', verticalalignment='center', fontsize=14) plt.axis('off') return fig # Get data for the required metrics revenue = statement.loc['Total Revenue'] gross_profit = statement.loc['Gross Profit'] operating_income = statement.loc['Operating Income'] net_income = statement.loc['Net Income'] # Calculate margins gross_margin = (gross_profit / revenue) * 100 operating_margin = (operating_income / revenue) * 100 net_margin = (net_income / revenue) * 100 # Create DataFrame for plotting margins_df = pd.DataFrame({ 'Gross Margin': gross_margin, 'Operating Margin': operating_margin, 'Net Margin': net_margin }) # Create the chart fig = plt.figure(figsize=(10, 6)) # Plot margins ax = margins_df.plot(kind='line', marker='o', ax=plt.gca()) # Format y-axis as percentage ax.yaxis.set_major_formatter(mtick.PercentFormatter()) # Add value labels at data points for line, margin_type in zip(ax.get_lines(), margins_df.columns): x_data, y_data = line.get_data() for x, y in zip(x_data, y_data): ax.annotate(f'{y:.1f}%', (x, y), textcoords="offset points", xytext=(0,5), ha='center', fontsize=8) plt.title(title, fontsize=14) plt.ylabel('Margin (%)') plt.grid(True, alpha=0.3) plt.legend(loc='best') plt.tight_layout() return fig def radar_factory(num_vars, frame='circle'): """Create a radar chart with `num_vars` axes.""" # Calculate evenly-spaced axis angles theta = np.linspace(0, 2*np.pi, num_vars, endpoint=False) class RadarAxes(PolarAxes): name = 'radar' def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Rotate plot so that first axis is at the top self.set_theta_zero_location('N') def fill(self, *args, closed=True, **kwargs): """Override fill so that line is closed by default""" return super().fill(closed=closed, *args, **kwargs) def plot(self, *args, **kwargs): """Override plot so that line is closed by default""" lines = super().plot(*args, **kwargs) for line in lines: self._close_line(line) return lines def _close_line(self, line): x, y = line.get_data() # FIXME: markers at x[0], y[0] get doubled-up if x[0] != x[-1]: x = np.append(x, x[0]) y = np.append(y, y[0]) line.set_data(x, y) def set_varlabels(self, labels): self.set_thetagrids(np.degrees(theta), labels) def _gen_axes_patch(self): # The Axes patch must be centered at (0.5, 0.5) and of radius 0.5 # in axes coordinates. if frame == 'circle': return Circle((0.5, 0.5), 0.5) elif frame == 'polygon': return RegularPolygon((0.5, 0.5), num_vars, radius=0.5, orientation=np.pi/2) else: raise ValueError("Unknown value for 'frame': %s" % frame) def _gen_axes_spines(self): if frame == 'circle': return super()._gen_axes_spines() elif frame == 'polygon': # spine_type must be 'left'/'right'/'top'/'bottom'/'circle'. spine = Spine(axes=self, spine_type='circle', path=Path.unit_regular_polygon(num_vars)) # unit_regular_polygon returns a polygon of radius 1 centered at # (0, 0) but we want a polygon of radius 0.5 centered at (0.5, # 0.5) in axes coordinates. spine.set_transform(Affine2D().scale(.5).translate(.5, .5) + self.transAxes) return {'polar': spine} else: raise ValueError("Unknown value for 'frame': %s" % frame) # Register the projection with Matplotlib register_projection(RadarAxes) return theta def create_spider_chart(metrics, title="Financial Metrics Comparison"): """Create a spider/radar chart for key financial metrics""" # Close any existing figures to prevent memory issues plt.close('all') # Select metrics to display on the spider chart - expanded list spider_metrics = { 'P/E Ratio': metrics.get('P/E Ratio', 'N/A'), 'P/B Ratio': metrics.get('P/B Ratio', 'N/A'), 'EV/EBITDA': metrics.get('EV/EBITDA', 'N/A'), 'PEG Ratio': metrics.get('PEG Ratio', 'N/A'), 'ROE (%)': metrics.get('ROE', 'N/A'), 'ROA (%)': metrics.get('ROA', 'N/A'), 'Profit Margin (%)': metrics.get('Profit Margin', 'N/A'), 'Operating Margin (%)': metrics.get('Operating Margin', 'N/A'), 'Debt to Equity': metrics.get('Debt to Equity', 'N/A'), 'Current Ratio': metrics.get('Current Ratio', 'N/A'), 'Dividend Yield (%)': metrics.get('Dividend Yield (%)', 'N/A'), 'Revenue Growth (%)': metrics.get('Revenue Growth', 'N/A') } # Filter out N/A values and prepare data filtered_metrics = {k: v for k, v in spider_metrics.items() if v != 'N/A' and v is not None} if len(filtered_metrics) < 3: # Not enough metrics for a meaningful spider chart fig = plt.figure(figsize=(10, 10)) plt.text(0.5, 0.5, "Insufficient data for spider chart", horizontalalignment='center', verticalalignment='center', fontsize=14) plt.axis('off') return fig # Prepare data for radar chart categories = list(filtered_metrics.keys()) N = len(categories) # Create radar chart theta = radar_factory(N, frame='polygon') # Normalize values for better visualization values = list(filtered_metrics.values()) # Define normalization parameters for each metric normalization_params = { 'P/E Ratio': {'better': 'lower', 'max': 50, 'min': 0}, 'P/B Ratio': {'better': 'lower', 'max': 10, 'min': 0}, 'EV/EBITDA': {'better': 'lower', 'max': 20, 'min': 0}, 'PEG Ratio': {'better': 'lower', 'max': 3, 'min': 0}, 'ROE (%)': {'better': 'higher', 'max': 30, 'min': 0}, 'ROA (%)': {'better': 'higher', 'max': 15, 'min': 0}, 'Profit Margin (%)': {'better': 'higher', 'max': 30, 'min': 0}, 'Operating Margin (%)': {'better': 'higher', 'max': 30, 'min': 0}, 'Debt to Equity': {'better': 'lower', 'max': 3, 'min': 0}, 'Current Ratio': {'better': 'higher', 'max': 3, 'min': 0}, 'Dividend Yield (%)': {'better': 'higher', 'max': 10, 'min': 0}, 'Revenue Growth (%)': {'better': 'higher', 'max': 30, 'min': 0} } # Normalize values normalized = [] for i, (cat, val) in enumerate(zip(categories, values)): params = normalization_params.get(cat, {'better': 'higher', 'max': 100, 'min': 0}) # Clip value to min/max range val = max(min(val, params['max']), params['min']) # Normalize to 0-1 scale if params['better'] == 'lower': # For metrics where lower is better, invert the scale norm_val = 1 - ((val - params['min']) / (params['max'] - params['min'])) else: # For metrics where higher is better norm_val = (val - params['min']) / (params['max'] - params['min']) normalized.append(norm_val) # Create the figure fig, ax = plt.subplots(figsize=(10, 10), subplot_kw=dict(projection='radar')) # Plot the data ax.plot(theta, normalized, 'o-', linewidth=2) ax.fill(theta, normalized, alpha=0.25) # Set labels ax.set_varlabels(categories) # Add values to the plot for i, (angle, radius) in enumerate(zip(theta, normalized)): ax.text(angle, radius + 0.1, f"{values[i]:.1f}", horizontalalignment='center', verticalalignment='center') # Add title plt.title(title, position=(0.5, 1.1), size=15) # Add a reference circle at 0.5 ax.plot(theta, [0.5]*N, '--', color='gray', alpha=0.75, linewidth=1) return fig def create_multi_year_growth_chart(statement, metrics, title, currency_symbol='$'): """Create a chart showing growth of multiple metrics over years""" # Close any existing figures to prevent memory issues plt.close('all') if statement is None or statement.empty: fig = plt.figure(figsize=(12, 6)) plt.text(0.5, 0.5, "No data available", horizontalalignment='center', verticalalignment='center', fontsize=14) plt.axis('off') return fig # Filter for metrics that exist in the statement available_metrics = [m for m in metrics if m in statement.index] if not available_metrics: fig = plt.figure(figsize=(12, 6)) plt.text(0.5, 0.5, "No relevant metrics found", horizontalalignment='center', verticalalignment='center', fontsize=14) plt.axis('off') return fig # Get data for the selected metrics data = statement.loc[available_metrics] # Convert to billions for better readability data = data / 1_000_000_000 # Create the chart fig = plt.figure(figsize=(12, 6)) # Plot as line chart ax = data.T.plot(kind='line', marker='o', ax=plt.gca()) # Add value labels at data points for line, metric in zip(ax.get_lines(), available_metrics): x_data, y_data = line.get_data() for x, y in zip(x_data, y_data): ax.annotate(f'{y:.1f}B', (x, y), textcoords="offset points", xytext=(0,5), ha='center', fontsize=8) plt.title(title, fontsize=14) plt.ylabel(f'Billions ({currency_symbol})') plt.grid(True, alpha=0.3) plt.legend(loc='best') plt.tight_layout() return fig def create_ratio_chart(statement, title, ratio_type='profitability'): """Create a chart showing financial ratios over time""" # Close any existing figures to prevent memory issues plt.close('all') if statement is None or statement.empty: fig = plt.figure(figsize=(10, 6)) plt.text(0.5, 0.5, "No data available", horizontalalignment='center', verticalalignment='center', fontsize=14) plt.axis('off') return fig # Define metrics based on ratio type if ratio_type == 'profitability': if 'Total Revenue' in statement.index and 'Net Income' in statement.index: revenue = statement.loc['Total Revenue'] net_income = statement.loc['Net Income'] net_margin = (net_income / revenue) * 100 if 'Gross Profit' in statement.index and 'Operating Income' in statement.index: gross_profit = statement.loc['Gross Profit'] operating_income = statement.loc['Operating Income'] gross_margin = (gross_profit / revenue) * 100 operating_margin = (operating_income / revenue) * 100 # Create DataFrame for plotting ratios_df = pd.DataFrame({ 'Gross Margin': gross_margin, 'Operating Margin': operating_margin, 'Net Margin': net_margin }) else: # Only net margin available ratios_df = pd.DataFrame({ 'Net Margin': net_margin }) else: fig = plt.figure(figsize=(10, 6)) plt.text(0.5, 0.5, "Insufficient data for profitability ratios", horizontalalignment='center', verticalalignment='center', fontsize=14) plt.axis('off') return fig elif ratio_type == 'efficiency': if 'Total Assets' in statement.index and 'Net Income' in statement.index: assets = statement.loc['Total Assets'] net_income = statement.loc['Net Income'] roa = (net_income / assets) * 100 if 'Total Equity Gross Minority Interest' in statement.index: equity = statement.loc['Total Equity Gross Minority Interest'] roe = (net_income / equity) * 100 # Create DataFrame for plotting ratios_df = pd.DataFrame({ 'Return on Assets': roa, 'Return on Equity': roe }) else: # Only ROA available ratios_df = pd.DataFrame({ 'Return on Assets': roa }) else: fig = plt.figure(figsize=(10, 6)) plt.text(0.5, 0.5, "Insufficient data for efficiency ratios", horizontalalignment='center', verticalalignment='center', fontsize=14) plt.axis('off') return fig elif ratio_type == 'liquidity': if 'Current Assets' in statement.index and 'Current Liabilities' in statement.index: current_assets = statement.loc['Current Assets'] current_liabilities = statement.loc['Current Liabilities'] current_ratio = current_assets / current_liabilities if 'Inventory' in statement.index: inventory = statement.loc['Inventory'] quick_ratio = (current_assets - inventory) / current_liabilities # Create DataFrame for plotting ratios_df = pd.DataFrame({ 'Current Ratio': current_ratio, 'Quick Ratio': quick_ratio }) else: # Only current ratio available ratios_df = pd.DataFrame({ 'Current Ratio': current_ratio }) else: fig = plt.figure(figsize=(10, 6)) plt.text(0.5, 0.5, "Insufficient data for liquidity ratios", horizontalalignment='center', verticalalignment='center', fontsize=14) plt.axis('off') return fig else: # Default case fig = plt.figure(figsize=(10, 6)) plt.text(0.5, 0.5, f"Unknown ratio type: {ratio_type}", horizontalalignment='center', verticalalignment='center', fontsize=14) plt.axis('off') return fig # Create the chart fig = plt.figure(figsize=(10, 6)) # Plot ratios ax = ratios_df.plot(kind='line', marker='o', ax=plt.gca()) # Format y-axis as percentage for profitability and efficiency ratios if ratio_type in ['profitability', 'efficiency']: ax.yaxis.set_major_formatter(mtick.PercentFormatter()) # Add value labels at data points for line, ratio_name in zip(ax.get_lines(), ratios_df.columns): x_data, y_data = line.get_data() for x, y in zip(x_data, y_data): if ratio_type in ['profitability', 'efficiency']: label = f'{y:.1f}%' else: label = f'{y:.2f}' ax.annotate(label, (x, y), textcoords="offset points", xytext=(0,5), ha='center', fontsize=8) plt.title(title, fontsize=14) if ratio_type == 'profitability': plt.ylabel('Margin (%)') elif ratio_type == 'efficiency': plt.ylabel('Return (%)') elif ratio_type == 'liquidity': plt.ylabel('Ratio') plt.grid(True, alpha=0.3) plt.legend(loc='best') plt.tight_layout() return fig