""" Dashboard visualization components for Wildberries Analytics Creates interactive charts and visualizations using Plotly """ import plotly.graph_objects as go import plotly.express as px from plotly.subplots import make_subplots import pandas as pd import numpy as np from datetime import datetime, timedelta from typing import Dict, List, Any, Optional import logging from wildberries_client import validate_wb_data, aggregate_wb_data logger = logging.getLogger(__name__) def create_sales_dashboard(sales_data: pd.DataFrame, period: str = "week", daily_revenue_data: pd.DataFrame = None) -> go.Figure: """ Create comprehensive sales dashboard with multiple visualizations Args: sales_data: DataFrame with sales data period: Analysis period ("week" or "month") daily_revenue_data: Pre-calculated daily revenue data (optional) Returns: Plotly figure with sales dashboard """ if sales_data.empty: return create_empty_chart("No sales data available") # Create subplot layout with better spacing fig = make_subplots( rows=3, cols=1, subplot_titles=[ "📈 Daily Revenue Trend", "🏆 Top Products by Revenue", "📊 Sales by Category" ], specs=[[{"secondary_y": True}], [{"type": "bar"}], [{"type": "pie"}]], vertical_spacing=0.15, row_heights=[0.4, 0.35, 0.25] ) try: # 1. Daily Revenue Trend (Top Left) - Use pre-calculated data if available if daily_revenue_data is not None and not daily_revenue_data.empty: # Use the pre-calculated daily revenue data to ensure consistency daily_revenue = daily_revenue_data.copy() elif 'sale_date' in sales_data.columns and 'total_price' in sales_data.columns: # Fallback to calculating if not provided daily_revenue = sales_data.groupby(sales_data['sale_date'].dt.date).agg({ 'total_price': 'sum', 'quantity': 'sum' }).reset_index() else: daily_revenue = pd.DataFrame() if not daily_revenue.empty: # Revenue line fig.add_trace( go.Scatter( x=daily_revenue['sale_date'], y=daily_revenue['total_price'], mode='lines+markers', name='Revenue (₽)', line=dict(color='#2E86AB', width=3), marker=dict(size=8), hovertemplate='%{x}
Revenue: ₽%{y:,.0f}' ), row=1, col=1 ) # Quantity bars on secondary y-axis fig.add_trace( go.Bar( x=daily_revenue['sale_date'], y=daily_revenue['quantity'], name='Quantity', marker_color='rgba(46, 134, 171, 0.3)', yaxis='y2', hovertemplate='%{x}
Quantity: %{y}' ), row=1, col=1, secondary_y=True ) # 2. Top Products by Revenue (Second Row) if 'product_name' in sales_data.columns and 'total_price' in sales_data.columns: top_products = sales_data.groupby('product_name')['total_price'].sum().nlargest(8).reset_index() fig.add_trace( go.Bar( x=top_products['total_price'], y=top_products['product_name'], orientation='h', name='Top Products', marker_color='#A23B72', hovertemplate='%{y}
Revenue: ₽%{x:,.0f}' ), row=2, col=1 ) # 3. Sales by Category (Third Row) if 'category' in sales_data.columns and 'total_price' in sales_data.columns: category_sales = sales_data.groupby('category')['total_price'].sum().reset_index() fig.add_trace( go.Pie( labels=category_sales['category'], values=category_sales['total_price'], name='Categories', hovertemplate='%{label}
Revenue: ₽%{value:,.0f}
Percent: %{percent}', marker_colors=px.colors.qualitative.Set3 ), row=3, col=1 ) except Exception as e: logger.error(f"Error creating sales dashboard: {str(e)}") return create_empty_chart(f"Error creating dashboard: {str(e)}") # Update layout fig.update_layout( title=f"📊 Sales Analytics Dashboard - Last {period.title()}", title_x=0.5, showlegend=False, height=1100, font=dict(size=12), template="plotly_white", margin=dict(t=120, b=80, l=80, r=80) ) # Update axes labels fig.update_xaxes(title_text="Date", row=1, col=1) fig.update_yaxes(title_text="Revenue (₽)", row=1, col=1) fig.update_yaxes(title_text="Quantity", secondary_y=True, row=1, col=1) fig.update_xaxes(title_text="Revenue (₽)", row=2, col=1) fig.update_yaxes(title_text="Products", row=2, col=1) return fig def create_inventory_dashboard(forecast_data: pd.DataFrame) -> go.Figure: """ Create inventory risk analysis dashboard Args: forecast_data: DataFrame with forecast results Returns: Plotly figure with inventory dashboard """ if forecast_data.empty: return create_empty_chart("No inventory data available") # Create subplot layout fig = make_subplots( rows=3, cols=1, subplot_titles=[ "🚨 Risk Level Distribution", "⏰ Days Until Stockout", "📦 Current Stock Levels" ], specs=[[{"type": "pie"}], [{"type": "bar"}], [{"type": "bar"}]], vertical_spacing=0.15, row_heights=[0.35, 0.35, 0.30] ) try: # 1. Risk Level Distribution (Top Left) if 'risk_level' in forecast_data.columns: risk_counts = forecast_data['risk_level'].value_counts() colors = { '🔴 Critical': '#FF4444', '🟡 Warning': '#FFAA00', '🟢 Safe': '#44AA44', '❌ Error': '#888888' } fig.add_trace( go.Pie( labels=risk_counts.index, values=risk_counts.values, name='Risk Levels', marker_colors=[colors.get(label, '#CCCCCC') for label in risk_counts.index], hovertemplate='%{label}
Count: %{value}
Percent: %{percent}' ), row=1, col=1 ) # 2. Days Until Stockout (Top Right) if 'days_until_stockout' in forecast_data.columns and 'product_name' in forecast_data.columns: # Sort by days until stockout and take top 15 for readability sorted_data = forecast_data.nsmallest(15, 'days_until_stockout') # Color bars based on risk level bar_colors = [] for days in sorted_data['days_until_stockout']: if days < 7: bar_colors.append('#FF4444') elif days < 14: bar_colors.append('#FFAA00') else: bar_colors.append('#44AA44') fig.add_trace( go.Bar( x=sorted_data['days_until_stockout'], y=sorted_data['product_name'], orientation='h', name='Days Until Stockout', marker_color=bar_colors, hovertemplate='%{y}
Days: %{x:.1f}' ), row=2, col=1 ) # 3. Current Stock Levels (Third Row) if 'current_stock' in forecast_data.columns and 'product_name' in forecast_data.columns: # Take top 10 products by stock level for better visibility stock_data = forecast_data.nlargest(10, 'current_stock') fig.add_trace( go.Bar( x=stock_data['product_name'], y=stock_data['current_stock'], name='Current Stock', marker_color='#2E86AB', hovertemplate='%{x}
Stock: %{y}' ), row=3, col=1 ) except Exception as e: logger.error(f"Error creating inventory dashboard: {str(e)}") return create_empty_chart(f"Error creating dashboard: {str(e)}") # Update layout fig.update_layout( title="📦 Inventory Risk Analysis Dashboard", title_x=0.5, showlegend=False, height=1100, font=dict(size=12), template="plotly_white", margin=dict(t=120, b=80, l=80, r=80) ) # Update axes fig.update_xaxes(title_text="Days", row=2, col=1) fig.update_yaxes(title_text="Products", row=2, col=1) fig.update_xaxes(title_text="Products", row=3, col=1, tickangle=45) fig.update_yaxes(title_text="Stock Quantity", row=3, col=1) return fig def create_trend_chart(data: pd.DataFrame, date_col: str = 'sale_date', value_col: str = 'total_price', title: str = "Trend Analysis") -> go.Figure: """ Create a trend analysis chart Args: data: DataFrame with time series data date_col: Name of date column value_col: Name of value column title: Chart title Returns: Plotly figure with trend chart """ if data.empty: return create_empty_chart("No data available for trend analysis") try: # Group by date if date_col in data.columns and value_col in data.columns: daily_data = data.groupby(data[date_col].dt.date)[value_col].sum().reset_index() fig = go.Figure() # Add main trend line fig.add_trace( go.Scatter( x=daily_data[date_col], y=daily_data[value_col], mode='lines+markers', name='Daily Values', line=dict(color='#2E86AB', width=3), marker=dict(size=8), hovertemplate='%{x}
Value: %{y:,.0f}' ) ) # Add moving average if enough data points if len(daily_data) >= 7: daily_data['ma7'] = daily_data[value_col].rolling(window=7, center=True).mean() fig.add_trace( go.Scatter( x=daily_data[date_col], y=daily_data['ma7'], mode='lines', name='7-day Moving Average', line=dict(color='#A23B72', width=2, dash='dash'), hovertemplate='%{x}
7-day MA: %{y:,.0f}' ) ) fig.update_layout( title=title, title_x=0.5, xaxis_title="Date", yaxis_title="Value", template="plotly_white", height=400 ) return fig else: return create_empty_chart(f"Required columns not found: {date_col}, {value_col}") except Exception as e: logger.error(f"Error creating trend chart: {str(e)}") return create_empty_chart(f"Error creating chart: {str(e)}") def create_comparison_chart(data: pd.DataFrame, category_col: str = 'category', value_col: str = 'total_price', title: str = "Category Comparison") -> go.Figure: """ Create a comparison chart for categories Args: data: DataFrame with categorical data category_col: Name of category column value_col: Name of value column title: Chart title Returns: Plotly figure with comparison chart """ if data.empty: return create_empty_chart("No data available for comparison") try: if category_col in data.columns and value_col in data.columns: category_data = data.groupby(category_col)[value_col].sum().sort_values(ascending=True) fig = go.Figure() fig.add_trace( go.Bar( x=category_data.values, y=category_data.index, orientation='h', marker_color=px.colors.qualitative.Set3, hovertemplate='%{y}
Value: %{x:,.0f}' ) ) fig.update_layout( title=title, title_x=0.5, xaxis_title="Value", yaxis_title="Category", template="plotly_white", height=400 ) return fig else: return create_empty_chart(f"Required columns not found: {category_col}, {value_col}") except Exception as e: logger.error(f"Error creating comparison chart: {str(e)}") return create_empty_chart(f"Error creating chart: {str(e)}") def create_empty_chart(message: str = "No data available") -> go.Figure: """ Create an empty chart with a message Args: message: Message to display Returns: Plotly figure with message """ fig = go.Figure() fig.add_annotation( x=0.5, y=0.5, xref="paper", yref="paper", text=message, showarrow=False, font=dict(size=16, color="gray"), xanchor="center", yanchor="middle" ) fig.update_layout( template="plotly_white", height=400, xaxis=dict(showgrid=False, showticklabels=False, zeroline=False), yaxis=dict(showgrid=False, showticklabels=False, zeroline=False) ) return fig def create_kpi_cards(data: pd.DataFrame) -> Dict[str, Any]: """ Calculate KPIs for dashboard cards Args: data: DataFrame with sales/inventory data Returns: Dictionary with KPI values """ if data.empty: return { "total_revenue": 0, "total_orders": 0, "avg_order_value": 0, "top_product": "N/A" } kpis = {} try: # Revenue metrics if 'total_price' in data.columns: kpis['total_revenue'] = data['total_price'].sum() kpis['avg_order_value'] = data['total_price'].mean() else: kpis['total_revenue'] = 0 kpis['avg_order_value'] = 0 # Order count (net orders: sales minus returns) if 'is_return' in data.columns: sales_count = (~data['is_return']).sum() returns_count = data['is_return'].sum() kpis['total_orders'] = sales_count - returns_count kpis['sales_count'] = sales_count kpis['returns_count'] = returns_count else: kpis['total_orders'] = len(data) # Top product if 'product_name' in data.columns and 'total_price' in data.columns: top_product = data.groupby('product_name')['total_price'].sum().idxmax() kpis['top_product'] = top_product else: kpis['top_product'] = "N/A" # Growth calculations (if date column available) if 'sale_date' in data.columns: # Calculate week-over-week growth current_week = data[data['sale_date'] >= (datetime.now() - timedelta(days=7))] previous_week = data[ (data['sale_date'] >= (datetime.now() - timedelta(days=14))) & (data['sale_date'] < (datetime.now() - timedelta(days=7))) ] current_revenue = current_week['total_price'].sum() if 'total_price' in data.columns else 0 previous_revenue = previous_week['total_price'].sum() if 'total_price' in data.columns else 0 if previous_revenue > 0: growth_rate = ((current_revenue - previous_revenue) / previous_revenue) * 100 kpis['revenue_growth'] = round(growth_rate, 1) else: kpis['revenue_growth'] = 0 except Exception as e: logger.error(f"Error calculating KPIs: {str(e)}") kpis = { "total_revenue": 0, "total_orders": 0, "avg_order_value": 0, "top_product": "Error calculating", "revenue_growth": 0 } return kpis def format_metric(value: float, metric_type: str = "currency") -> str: """ Format metrics for display Args: value: Numeric value to format metric_type: Type of metric ("currency", "number", "percent") Returns: Formatted string """ if pd.isna(value) or value is None: return "N/A" try: if metric_type == "currency": return f"₽{value:,.0f}" elif metric_type == "percent": return f"{value:+.1f}%" elif metric_type == "number": return f"{value:,.0f}" else: return str(value) except: return "N/A" def create_wb_kpi_cards(data: pd.DataFrame) -> Dict[str, Any]: """Enhanced KPIs specifically for Wildberries data""" kpis = create_kpi_cards(data) # Use existing function try: # Add Wildberries-specific metrics if 'sales_commission' in data.columns: total_commission = data['sales_commission'].sum() kpis['total_commission'] = total_commission if 'total_price' in data.columns and data['total_price'].sum() > 0: commission_rate = (total_commission / data['total_price'].sum() * 100) kpis['avg_commission_rate'] = commission_rate else: kpis['avg_commission_rate'] = 0 if 'amount_for_pay' in data.columns: kpis['total_payout'] = data['amount_for_pay'].sum() # Calculate net profit (amount_for_pay is what seller receives) if 'total_price' in data.columns: gross_revenue = data['total_price'].sum() net_revenue = data['amount_for_pay'].sum() kpis['platform_fees'] = gross_revenue - net_revenue kpis['net_margin_percent'] = (net_revenue / gross_revenue * 100) if gross_revenue > 0 else 0 if 'office_name' in data.columns: office_sales = data.groupby('office_name')['total_price'].sum() if not office_sales.empty: kpis['top_office'] = office_sales.idxmax() kpis['top_office_revenue'] = office_sales.max() if 'delivery_cost' in data.columns: kpis['total_delivery_cost'] = data['delivery_cost'].sum() # Calculate sales velocity (average daily sales) if 'sale_date' in data.columns and not data.empty: date_range = (data['sale_date'].max() - data['sale_date'].min()).days if date_range > 0: kpis['daily_sales_velocity'] = len(data) / date_range else: kpis['daily_sales_velocity'] = len(data) except Exception as e: logger.error(f"Error calculating Wildberries KPIs: {str(e)}") return kpis def create_commission_analysis_chart(sales_data: pd.DataFrame) -> go.Figure: """Create commission analysis visualization for Wildberries data""" if sales_data.empty or 'sales_commission' not in sales_data.columns: return create_empty_chart("No commission data available") fig = make_subplots( rows=2, cols=2, subplot_titles=[ "Commission by Product", "Commission Rate Trend", "Revenue vs Commission", "Top Commission Products" ], specs=[[{"type": "bar"}, {"type": "scatter"}], [{"type": "scatter"}, {"type": "bar"}]], vertical_spacing=0.1, horizontal_spacing=0.1 ) try: # 1. Commission by Product (Top 10) if 'product_name' in sales_data.columns: product_commission = sales_data.groupby('product_name')['sales_commission'].sum().nlargest(10) fig.add_trace( go.Bar( x=product_commission.index, y=product_commission.values, name='Commission', marker_color='#FF6B6B', hovertemplate='%{x}
Commission: ₽%{y:,.0f}' ), row=1, col=1 ) # 2. Daily commission trend if 'sale_date' in sales_data.columns: daily_commission = sales_data.groupby(sales_data['sale_date'].dt.date)['sales_commission'].sum() fig.add_trace( go.Scatter( x=daily_commission.index, y=daily_commission.values, mode='lines+markers', name='Daily Commission', line=dict(color='#4ECDC4', width=2), hovertemplate='%{x}
Commission: ₽%{y:,.0f}' ), row=1, col=2 ) # 3. Revenue vs Commission scatter if 'total_price' in sales_data.columns and 'product_name' in sales_data.columns: product_data = sales_data.groupby('product_name').agg({ 'total_price': 'sum', 'sales_commission': 'sum' }).reset_index() fig.add_trace( go.Scatter( x=product_data['total_price'], y=product_data['sales_commission'], mode='markers', name='Revenue vs Commission', marker=dict( size=8, color='#45B7D1', opacity=0.7 ), text=product_data['product_name'], hovertemplate='%{text}
Revenue: ₽%{x:,.0f}
Commission: ₽%{y:,.0f}' ), row=2, col=1 ) # 4. Commission rate by product if 'product_name' in sales_data.columns and 'total_price' in sales_data.columns: product_stats = sales_data.groupby('product_name').agg({ 'total_price': 'sum', 'sales_commission': 'sum' }) product_stats['commission_rate'] = (product_stats['sales_commission'] / product_stats['total_price'] * 100) top_rates = product_stats['commission_rate'].nlargest(10) fig.add_trace( go.Bar( x=top_rates.index, y=top_rates.values, name='Commission Rate (%)', marker_color='#96CEB4', hovertemplate='%{x}
Commission Rate: %{y:.1f}%' ), row=2, col=2 ) except Exception as e: logger.error(f"Error creating commission analysis: {str(e)}") return create_empty_chart(f"Error creating commission analysis: {str(e)}") # Update layout fig.update_layout( title="💰 Commission Analysis Dashboard", title_x=0.5, showlegend=False, height=800, font=dict(size=10), template="plotly_white" ) # Update axes fig.update_xaxes(title_text="Products", row=1, col=1) fig.update_yaxes(title_text="Commission (₽)", row=1, col=1) fig.update_xaxes(title_text="Date", row=1, col=2) fig.update_yaxes(title_text="Commission (₽)", row=1, col=2) fig.update_xaxes(title_text="Revenue (₽)", row=2, col=1) fig.update_yaxes(title_text="Commission (₽)", row=2, col=1) fig.update_xaxes(title_text="Products", row=2, col=2) fig.update_yaxes(title_text="Commission Rate (%)", row=2, col=2) return fig def validate_and_process_wb_data(weekly_data: pd.DataFrame, monthly_data: pd.DataFrame) -> Dict[str, Any]: """Validate and process Wildberries data with enhanced error handling""" # Run validation validation_results = validate_wb_data(weekly_data, monthly_data) # Process data for better performance if large datasets processed_data = { "weekly": weekly_data, "monthly": monthly_data, "validation": validation_results } # Aggregate data if datasets are large (>1000 records) if len(weekly_data) > 1000: processed_data["weekly_aggregated"] = aggregate_wb_data(weekly_data, 'daily') logger.info(f"Aggregated weekly data from {len(weekly_data)} to {len(processed_data['weekly_aggregated'])} records") if len(monthly_data) > 1000: processed_data["monthly_aggregated"] = aggregate_wb_data(monthly_data, 'daily') logger.info(f"Aggregated monthly data from {len(monthly_data)} to {len(processed_data['monthly_aggregated'])} records") return processed_data