2025-06-03 13:56:15 +08:00
|
|
|
"""
|
|
|
|
|
Error Handling Utilities for Chart Layers
|
|
|
|
|
|
|
|
|
|
This module provides comprehensive error handling for chart creation,
|
|
|
|
|
including custom exceptions, error recovery strategies, and user-friendly
|
|
|
|
|
error messaging for various insufficient data scenarios.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import pandas as pd
|
|
|
|
|
from datetime import datetime, timezone, timedelta
|
|
|
|
|
from typing import List, Dict, Any, Optional, Union, Tuple, Callable
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
from enum import Enum
|
|
|
|
|
|
|
|
|
|
from utils.logger import get_logger
|
|
|
|
|
|
|
|
|
|
# Initialize logger
|
2025-06-12 13:27:30 +08:00
|
|
|
logger = get_logger()
|
2025-06-03 13:56:15 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class ErrorSeverity(Enum):
|
|
|
|
|
"""Error severity levels for chart operations"""
|
|
|
|
|
INFO = "info" # Informational, chart can proceed
|
|
|
|
|
WARNING = "warning" # Warning, chart proceeds with limitations
|
|
|
|
|
ERROR = "error" # Error, chart creation may fail
|
|
|
|
|
CRITICAL = "critical" # Critical error, chart creation impossible
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class ChartError:
|
|
|
|
|
"""Container for chart error information"""
|
|
|
|
|
code: str
|
|
|
|
|
message: str
|
|
|
|
|
severity: ErrorSeverity
|
|
|
|
|
context: Dict[str, Any]
|
|
|
|
|
recovery_suggestion: Optional[str] = None
|
|
|
|
|
|
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
|
|
|
"""Convert error to dictionary for logging/serialization"""
|
|
|
|
|
return {
|
|
|
|
|
'code': self.code,
|
|
|
|
|
'message': self.message,
|
|
|
|
|
'severity': self.severity.value,
|
|
|
|
|
'context': self.context,
|
|
|
|
|
'recovery_suggestion': self.recovery_suggestion
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ChartDataError(Exception):
|
|
|
|
|
"""Base exception for chart data-related errors"""
|
|
|
|
|
def __init__(self, error: ChartError):
|
|
|
|
|
self.error = error
|
|
|
|
|
super().__init__(error.message)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class InsufficientDataError(ChartDataError):
|
|
|
|
|
"""Raised when there's insufficient data for chart/indicator calculations"""
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DataValidationError(ChartDataError):
|
|
|
|
|
"""Raised when data validation fails"""
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class IndicatorCalculationError(ChartDataError):
|
|
|
|
|
"""Raised when indicator calculations fail"""
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DataConnectionError(ChartDataError):
|
|
|
|
|
"""Raised when database/data source connection fails"""
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DataRequirements:
|
|
|
|
|
"""Data requirements checker for charts and indicators"""
|
|
|
|
|
|
|
|
|
|
# Minimum data requirements for different indicators
|
|
|
|
|
INDICATOR_MIN_PERIODS = {
|
|
|
|
|
'sma': lambda period: period + 5, # SMA needs period + buffer
|
|
|
|
|
'ema': lambda period: period * 2, # EMA needs 2x period for stability
|
|
|
|
|
'rsi': lambda period: period + 10, # RSI needs period + warmup
|
|
|
|
|
'macd': lambda fast, slow, signal: slow + signal + 10, # MACD most demanding
|
|
|
|
|
'bollinger_bands': lambda period: period + 5, # BB needs period + buffer
|
|
|
|
|
'candlestick': lambda: 10, # Basic candlestick minimum
|
|
|
|
|
'volume': lambda: 5 # Volume minimum
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def check_candlestick_requirements(cls, data_count: int) -> ChartError:
|
|
|
|
|
"""Check if we have enough data for basic candlestick chart"""
|
|
|
|
|
min_required = cls.INDICATOR_MIN_PERIODS['candlestick']()
|
|
|
|
|
|
|
|
|
|
if data_count == 0:
|
|
|
|
|
return ChartError(
|
|
|
|
|
code='NO_DATA',
|
|
|
|
|
message='No market data available',
|
|
|
|
|
severity=ErrorSeverity.CRITICAL,
|
|
|
|
|
context={'data_count': data_count, 'required': min_required},
|
|
|
|
|
recovery_suggestion='Check data collection service or select different symbol/timeframe'
|
|
|
|
|
)
|
|
|
|
|
elif data_count < min_required:
|
|
|
|
|
return ChartError(
|
|
|
|
|
code='INSUFFICIENT_CANDLESTICK_DATA',
|
|
|
|
|
message=f'Insufficient data for candlestick chart: {data_count} candles (need {min_required})',
|
|
|
|
|
severity=ErrorSeverity.WARNING,
|
|
|
|
|
context={'data_count': data_count, 'required': min_required},
|
|
|
|
|
recovery_suggestion='Chart will display with limited data - consider longer time range'
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
return ChartError(
|
|
|
|
|
code='SUFFICIENT_DATA',
|
|
|
|
|
message='Sufficient data for candlestick chart',
|
|
|
|
|
severity=ErrorSeverity.INFO,
|
|
|
|
|
context={'data_count': data_count, 'required': min_required}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def check_indicator_requirements(cls, indicator_type: str, data_count: int,
|
|
|
|
|
parameters: Dict[str, Any]) -> ChartError:
|
|
|
|
|
"""Check if we have enough data for specific indicator"""
|
|
|
|
|
if indicator_type not in cls.INDICATOR_MIN_PERIODS:
|
|
|
|
|
return ChartError(
|
|
|
|
|
code='UNKNOWN_INDICATOR',
|
|
|
|
|
message=f'Unknown indicator type: {indicator_type}',
|
|
|
|
|
severity=ErrorSeverity.ERROR,
|
|
|
|
|
context={'indicator_type': indicator_type, 'data_count': data_count},
|
|
|
|
|
recovery_suggestion='Check indicator type spelling or implementation'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Calculate minimum required data
|
|
|
|
|
try:
|
|
|
|
|
if indicator_type in ['sma', 'ema', 'rsi', 'bollinger_bands']:
|
|
|
|
|
period = parameters.get('period', 20)
|
|
|
|
|
min_required = cls.INDICATOR_MIN_PERIODS[indicator_type](period)
|
|
|
|
|
elif indicator_type == 'macd':
|
|
|
|
|
fast = parameters.get('fast_period', 12)
|
|
|
|
|
slow = parameters.get('slow_period', 26)
|
|
|
|
|
signal = parameters.get('signal_period', 9)
|
|
|
|
|
min_required = cls.INDICATOR_MIN_PERIODS[indicator_type](fast, slow, signal)
|
|
|
|
|
else:
|
|
|
|
|
min_required = cls.INDICATOR_MIN_PERIODS[indicator_type]()
|
|
|
|
|
except Exception as e:
|
|
|
|
|
return ChartError(
|
|
|
|
|
code='PARAMETER_ERROR',
|
|
|
|
|
message=f'Invalid parameters for {indicator_type}: {e}',
|
|
|
|
|
severity=ErrorSeverity.ERROR,
|
|
|
|
|
context={'indicator_type': indicator_type, 'parameters': parameters},
|
|
|
|
|
recovery_suggestion='Check indicator parameters for valid values'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if data_count < min_required:
|
|
|
|
|
# Determine severity based on how insufficient the data is
|
|
|
|
|
if data_count < min_required // 2:
|
|
|
|
|
# Severely insufficient - less than half the required data
|
|
|
|
|
severity = ErrorSeverity.ERROR
|
|
|
|
|
else:
|
|
|
|
|
# Slightly insufficient - can potentially adjust parameters
|
|
|
|
|
severity = ErrorSeverity.WARNING
|
|
|
|
|
|
|
|
|
|
return ChartError(
|
|
|
|
|
code='INSUFFICIENT_INDICATOR_DATA',
|
|
|
|
|
message=f'Insufficient data for {indicator_type}: {data_count} candles (need {min_required})',
|
|
|
|
|
severity=severity,
|
|
|
|
|
context={
|
|
|
|
|
'indicator_type': indicator_type,
|
|
|
|
|
'data_count': data_count,
|
|
|
|
|
'required': min_required,
|
|
|
|
|
'parameters': parameters
|
|
|
|
|
},
|
|
|
|
|
recovery_suggestion=f'Increase data range to at least {min_required} candles or adjust {indicator_type} parameters'
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
return ChartError(
|
|
|
|
|
code='SUFFICIENT_INDICATOR_DATA',
|
|
|
|
|
message=f'Sufficient data for {indicator_type}',
|
|
|
|
|
severity=ErrorSeverity.INFO,
|
|
|
|
|
context={
|
|
|
|
|
'indicator_type': indicator_type,
|
|
|
|
|
'data_count': data_count,
|
|
|
|
|
'required': min_required
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ErrorRecoveryStrategies:
|
|
|
|
|
"""Error recovery strategies for different chart scenarios"""
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def handle_insufficient_data(error: ChartError, fallback_options: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
|
|
"""Handle insufficient data by providing fallback strategies"""
|
|
|
|
|
strategy = {
|
|
|
|
|
'can_proceed': False,
|
|
|
|
|
'fallback_action': None,
|
|
|
|
|
'modified_config': None,
|
|
|
|
|
'user_message': error.message
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if error.code == 'INSUFFICIENT_CANDLESTICK_DATA':
|
|
|
|
|
# For candlestick, we can proceed with warnings
|
|
|
|
|
strategy.update({
|
|
|
|
|
'can_proceed': True,
|
|
|
|
|
'fallback_action': 'display_with_warning',
|
|
|
|
|
'user_message': f"{error.message}. Chart will display available data."
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
elif error.code == 'INSUFFICIENT_INDICATOR_DATA':
|
|
|
|
|
# For indicators, try to adjust parameters or skip
|
|
|
|
|
indicator_type = error.context.get('indicator_type')
|
|
|
|
|
data_count = error.context.get('data_count', 0)
|
|
|
|
|
|
|
|
|
|
if indicator_type in ['sma', 'ema', 'bollinger_bands']:
|
|
|
|
|
# Try reducing period to fit available data
|
|
|
|
|
max_period = max(5, data_count // 2) # Conservative estimate
|
|
|
|
|
strategy.update({
|
|
|
|
|
'can_proceed': True,
|
|
|
|
|
'fallback_action': 'adjust_parameters',
|
|
|
|
|
'modified_config': {'period': max_period},
|
|
|
|
|
'user_message': f"Adjusted {indicator_type} period to {max_period} due to limited data"
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
elif indicator_type == 'rsi':
|
|
|
|
|
# RSI can work with reduced period
|
|
|
|
|
max_period = max(7, data_count // 3)
|
|
|
|
|
strategy.update({
|
|
|
|
|
'can_proceed': True,
|
|
|
|
|
'fallback_action': 'adjust_parameters',
|
|
|
|
|
'modified_config': {'period': max_period},
|
|
|
|
|
'user_message': f"Adjusted RSI period to {max_period} due to limited data"
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
# Skip the indicator entirely
|
|
|
|
|
strategy.update({
|
|
|
|
|
'can_proceed': True,
|
|
|
|
|
'fallback_action': 'skip_indicator',
|
|
|
|
|
'user_message': f"Skipped {indicator_type} due to insufficient data"
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return strategy
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def handle_data_validation_error(error: ChartError) -> Dict[str, Any]:
|
|
|
|
|
"""Handle data validation errors"""
|
|
|
|
|
return {
|
|
|
|
|
'can_proceed': False,
|
|
|
|
|
'fallback_action': 'show_error',
|
|
|
|
|
'user_message': f"Data validation failed: {error.message}",
|
|
|
|
|
'recovery_suggestion': error.recovery_suggestion
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def handle_connection_error(error: ChartError) -> Dict[str, Any]:
|
|
|
|
|
"""Handle database/connection errors"""
|
|
|
|
|
return {
|
|
|
|
|
'can_proceed': False,
|
|
|
|
|
'fallback_action': 'show_error',
|
|
|
|
|
'user_message': "Unable to connect to data source",
|
|
|
|
|
'recovery_suggestion': "Check database connection or try again later"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ChartErrorHandler:
|
|
|
|
|
"""Main error handler for chart operations"""
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
self.logger = logger
|
|
|
|
|
self.errors: List[ChartError] = []
|
|
|
|
|
self.warnings: List[ChartError] = []
|
|
|
|
|
|
|
|
|
|
def clear_errors(self):
|
|
|
|
|
"""Clear accumulated errors and warnings"""
|
|
|
|
|
self.errors.clear()
|
|
|
|
|
self.warnings.clear()
|
|
|
|
|
|
|
|
|
|
def validate_data_sufficiency(self, data: Union[pd.DataFrame, List[Dict[str, Any]]],
|
|
|
|
|
chart_type: str = 'candlestick',
|
|
|
|
|
indicators: List[Dict[str, Any]] = None) -> bool:
|
|
|
|
|
"""
|
|
|
|
|
Validate if data is sufficient for chart and indicator requirements.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
data: Chart data (DataFrame or list of candle dicts)
|
|
|
|
|
chart_type: Type of chart being created
|
|
|
|
|
indicators: List of indicator configurations
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
True if data is sufficient, False otherwise
|
|
|
|
|
"""
|
|
|
|
|
self.clear_errors()
|
|
|
|
|
|
|
|
|
|
# Get data count
|
|
|
|
|
if isinstance(data, pd.DataFrame):
|
|
|
|
|
data_count = len(data)
|
|
|
|
|
elif isinstance(data, list):
|
|
|
|
|
data_count = len(data)
|
|
|
|
|
else:
|
|
|
|
|
self.errors.append(ChartError(
|
|
|
|
|
code='INVALID_DATA_TYPE',
|
|
|
|
|
message=f'Invalid data type: {type(data)}',
|
|
|
|
|
severity=ErrorSeverity.ERROR,
|
|
|
|
|
context={'data_type': str(type(data))}
|
|
|
|
|
))
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
# Check basic chart requirements
|
|
|
|
|
chart_error = DataRequirements.check_candlestick_requirements(data_count)
|
|
|
|
|
if chart_error.severity in [ErrorSeverity.WARNING]:
|
|
|
|
|
self.warnings.append(chart_error)
|
|
|
|
|
elif chart_error.severity in [ErrorSeverity.ERROR, ErrorSeverity.CRITICAL]:
|
|
|
|
|
self.errors.append(chart_error)
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
# Check indicator requirements
|
|
|
|
|
if indicators:
|
|
|
|
|
for indicator_config in indicators:
|
|
|
|
|
indicator_type = indicator_config.get('type', 'unknown')
|
|
|
|
|
parameters = indicator_config.get('parameters', {})
|
|
|
|
|
|
|
|
|
|
indicator_error = DataRequirements.check_indicator_requirements(
|
|
|
|
|
indicator_type, data_count, parameters
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if indicator_error.severity == ErrorSeverity.WARNING:
|
|
|
|
|
self.warnings.append(indicator_error)
|
|
|
|
|
elif indicator_error.severity in [ErrorSeverity.ERROR, ErrorSeverity.CRITICAL]:
|
|
|
|
|
self.errors.append(indicator_error)
|
|
|
|
|
|
|
|
|
|
# Return True if no critical errors
|
|
|
|
|
return len(self.errors) == 0
|
|
|
|
|
|
|
|
|
|
def get_error_summary(self) -> Dict[str, Any]:
|
|
|
|
|
"""Get summary of all errors and warnings"""
|
|
|
|
|
return {
|
|
|
|
|
'has_errors': len(self.errors) > 0,
|
|
|
|
|
'has_warnings': len(self.warnings) > 0,
|
|
|
|
|
'error_count': len(self.errors),
|
|
|
|
|
'warning_count': len(self.warnings),
|
|
|
|
|
'errors': [error.to_dict() for error in self.errors],
|
|
|
|
|
'warnings': [warning.to_dict() for warning in self.warnings],
|
|
|
|
|
'can_proceed': len(self.errors) == 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def get_user_friendly_message(self) -> str:
|
|
|
|
|
"""Get a user-friendly message summarizing errors and warnings"""
|
|
|
|
|
if not self.errors and not self.warnings:
|
|
|
|
|
return "Chart data is ready"
|
|
|
|
|
|
|
|
|
|
messages = []
|
|
|
|
|
|
|
|
|
|
if self.errors:
|
|
|
|
|
error_msg = f"❌ {len(self.errors)} error(s) prevent chart creation"
|
|
|
|
|
messages.append(error_msg)
|
|
|
|
|
|
|
|
|
|
# Add most relevant error message
|
|
|
|
|
if self.errors:
|
|
|
|
|
main_error = self.errors[0] # Show first error
|
|
|
|
|
messages.append(f"• {main_error.message}")
|
|
|
|
|
if main_error.recovery_suggestion:
|
|
|
|
|
messages.append(f" 💡 {main_error.recovery_suggestion}")
|
|
|
|
|
|
|
|
|
|
if self.warnings:
|
|
|
|
|
warning_msg = f"⚠️ {len(self.warnings)} warning(s)"
|
|
|
|
|
messages.append(warning_msg)
|
|
|
|
|
|
|
|
|
|
# Add most relevant warning
|
|
|
|
|
if self.warnings:
|
|
|
|
|
main_warning = self.warnings[0]
|
|
|
|
|
messages.append(f"• {main_warning.message}")
|
|
|
|
|
|
|
|
|
|
return "\n".join(messages)
|
|
|
|
|
|
|
|
|
|
def apply_error_recovery(self, error: ChartError,
|
|
|
|
|
fallback_options: Dict[str, Any] = None) -> Dict[str, Any]:
|
|
|
|
|
"""Apply error recovery strategy for a specific error"""
|
|
|
|
|
fallback_options = fallback_options or {}
|
|
|
|
|
|
|
|
|
|
if error.code.startswith('INSUFFICIENT'):
|
|
|
|
|
return ErrorRecoveryStrategies.handle_insufficient_data(error, fallback_options)
|
|
|
|
|
elif 'VALIDATION' in error.code:
|
|
|
|
|
return ErrorRecoveryStrategies.handle_data_validation_error(error)
|
|
|
|
|
elif 'CONNECTION' in error.code:
|
|
|
|
|
return ErrorRecoveryStrategies.handle_connection_error(error)
|
|
|
|
|
else:
|
|
|
|
|
# Default recovery strategy
|
|
|
|
|
return {
|
|
|
|
|
'can_proceed': False,
|
|
|
|
|
'fallback_action': 'show_error',
|
|
|
|
|
'user_message': error.message,
|
|
|
|
|
'recovery_suggestion': error.recovery_suggestion
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Convenience functions
|
|
|
|
|
def check_data_sufficiency(data: Union[pd.DataFrame, List[Dict[str, Any]]],
|
|
|
|
|
indicators: List[Dict[str, Any]] = None) -> Tuple[bool, Dict[str, Any]]:
|
|
|
|
|
"""
|
|
|
|
|
Convenience function to check data sufficiency.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
data: Chart data
|
|
|
|
|
indicators: List of indicator configurations
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Tuple of (is_sufficient, error_summary)
|
|
|
|
|
"""
|
|
|
|
|
handler = ChartErrorHandler()
|
|
|
|
|
is_sufficient = handler.validate_data_sufficiency(data, indicators=indicators)
|
|
|
|
|
return is_sufficient, handler.get_error_summary()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_error_message(data: Union[pd.DataFrame, List[Dict[str, Any]]],
|
|
|
|
|
indicators: List[Dict[str, Any]] = None) -> str:
|
|
|
|
|
"""
|
|
|
|
|
Get user-friendly error message for data issues.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
data: Chart data
|
|
|
|
|
indicators: List of indicator configurations
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
User-friendly error message
|
|
|
|
|
"""
|
|
|
|
|
handler = ChartErrorHandler()
|
|
|
|
|
handler.validate_data_sufficiency(data, indicators=indicators)
|
|
|
|
|
return handler.get_user_friendly_message()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_error_annotation(error_message: str, position: str = "top") -> Dict[str, Any]:
|
|
|
|
|
"""
|
|
|
|
|
Create a Plotly annotation for error display.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
error_message: Error message to display
|
|
|
|
|
position: Position of annotation ('top', 'center', 'bottom')
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Plotly annotation configuration
|
|
|
|
|
"""
|
|
|
|
|
positions = {
|
|
|
|
|
'top': {'x': 0.5, 'y': 0.9},
|
|
|
|
|
'center': {'x': 0.5, 'y': 0.5},
|
|
|
|
|
'bottom': {'x': 0.5, 'y': 0.1}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pos = positions.get(position, positions['center'])
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
'text': error_message,
|
|
|
|
|
'xref': 'paper',
|
|
|
|
|
'yref': 'paper',
|
|
|
|
|
'x': pos['x'],
|
|
|
|
|
'y': pos['y'],
|
|
|
|
|
'xanchor': 'center',
|
|
|
|
|
'yanchor': 'middle',
|
|
|
|
|
'showarrow': False,
|
|
|
|
|
'font': {'size': 14, 'color': '#e74c3c'},
|
|
|
|
|
'bgcolor': 'rgba(255,255,255,0.8)',
|
|
|
|
|
'bordercolor': '#e74c3c',
|
|
|
|
|
'borderwidth': 1
|
|
|
|
|
}
|