| """ |
| Sentiment visualization components using Plotly |
| Creates interactive charts for sentiment analysis |
| """ |
| import plotly.graph_objects as go |
| import plotly.express as px |
| from plotly.subplots import make_subplots |
| import pandas as pd |
| import json |
| from pathlib import Path |
|
|
|
|
| class SentimentCharts: |
| """ |
| Creates sentiment-related visualizations |
| """ |
|
|
| def __init__(self, config_path=None): |
| """ |
| Initialize with configuration |
| |
| Args: |
| config_path: Path to configuration file |
| """ |
| if config_path is None: |
| config_path = Path(__file__).parent.parent / "config" / "viz_config.json" |
|
|
| with open(config_path, 'r') as f: |
| self.config = json.load(f) |
|
|
| self.sentiment_colors = self.config['color_schemes']['sentiment_polarity'] |
| self.sentiment_order = self.config['sentiment_order'] |
| self.chart_height = self.config['dashboard']['chart_height'] |
|
|
| def create_sentiment_pie_chart(self, df, title="Sentiment Distribution"): |
| """ |
| Create pie chart for sentiment distribution |
| |
| Args: |
| df: Sentiment dataframe |
| title: Chart title |
| |
| Returns: |
| plotly.graph_objects.Figure |
| """ |
| sentiment_counts = df['sentiment_polarity'].value_counts() |
|
|
| |
| ordered_sentiments = [s for s in self.sentiment_order if s in sentiment_counts.index] |
| sentiment_counts = sentiment_counts[ordered_sentiments] |
|
|
| colors = [self.sentiment_colors.get(s, '#CCCCCC') for s in sentiment_counts.index] |
|
|
| fig = go.Figure(data=[go.Pie( |
| labels=sentiment_counts.index, |
| values=sentiment_counts.values, |
| marker=dict(colors=colors), |
| textinfo='label+percent', |
| textposition='auto', |
| hovertemplate='<b>%{label}</b><br>Count: %{value}<br>Percentage: %{percent}<extra></extra>' |
| )]) |
|
|
| fig.update_layout( |
| title=title, |
| height=self.chart_height, |
| showlegend=True, |
| legend=dict(orientation="v", yanchor="middle", y=0.5, xanchor="left", x=1.05) |
| ) |
|
|
| return fig |
|
|
| def create_sentiment_bar_chart(self, df, group_by, title="Sentiment Distribution"): |
| """ |
| Create stacked bar chart for sentiment distribution by group |
| |
| Args: |
| df: Sentiment dataframe |
| group_by: Column to group by |
| title: Chart title |
| |
| Returns: |
| plotly.graph_objects.Figure |
| """ |
| |
| sentiment_pivot = pd.crosstab(df[group_by], df['sentiment_polarity']) |
|
|
| |
| ordered_columns = [s for s in self.sentiment_order if s in sentiment_pivot.columns] |
| sentiment_pivot = sentiment_pivot[ordered_columns] |
|
|
| fig = go.Figure() |
|
|
| for sentiment in sentiment_pivot.columns: |
| fig.add_trace(go.Bar( |
| name=sentiment, |
| x=sentiment_pivot.index, |
| y=sentiment_pivot[sentiment], |
| marker_color=self.sentiment_colors.get(sentiment, '#CCCCCC'), |
| hovertemplate='<b>%{x}</b><br>%{y} comments<extra></extra>' |
| )) |
|
|
| fig.update_layout( |
| title=title, |
| xaxis_title=group_by.capitalize(), |
| yaxis_title="Number of Comments", |
| barmode='stack', |
| height=self.chart_height, |
| legend=dict(title="Sentiment", orientation="v", yanchor="top", y=1, xanchor="left", x=1.02) |
| ) |
|
|
| return fig |
|
|
| def create_sentiment_percentage_bar_chart(self, df, group_by, title="Sentiment Distribution (%)"): |
| """ |
| Create 100% stacked bar chart for sentiment distribution |
| |
| Args: |
| df: Sentiment dataframe |
| group_by: Column to group by |
| title: Chart title |
| |
| Returns: |
| plotly.graph_objects.Figure |
| """ |
| |
| sentiment_pivot = pd.crosstab(df[group_by], df['sentiment_polarity'], normalize='index') * 100 |
|
|
| |
| ordered_columns = [s for s in self.sentiment_order if s in sentiment_pivot.columns] |
| sentiment_pivot = sentiment_pivot[ordered_columns] |
|
|
| fig = go.Figure() |
|
|
| for sentiment in sentiment_pivot.columns: |
| fig.add_trace(go.Bar( |
| name=sentiment, |
| x=sentiment_pivot.index, |
| y=sentiment_pivot[sentiment], |
| marker_color=self.sentiment_colors.get(sentiment, '#CCCCCC'), |
| hovertemplate='<b>%{x}</b><br>%{y:.1f}%<extra></extra>' |
| )) |
|
|
| fig.update_layout( |
| title=title, |
| xaxis_title=group_by.capitalize(), |
| yaxis_title="Percentage (%)", |
| barmode='stack', |
| height=self.chart_height, |
| yaxis=dict(range=[0, 100]), |
| legend=dict(title="Sentiment", orientation="v", yanchor="top", y=1, xanchor="left", x=1.02) |
| ) |
|
|
| return fig |
|
|
| def create_sentiment_heatmap(self, df, row_dimension, col_dimension, title="Sentiment Heatmap"): |
| """ |
| Create heatmap showing sentiment distribution across two dimensions |
| |
| Args: |
| df: Sentiment dataframe |
| row_dimension: Row dimension |
| col_dimension: Column dimension |
| title: Chart title |
| |
| Returns: |
| plotly.graph_objects.Figure |
| """ |
| |
| negative_sentiments = self.config['negative_sentiments'] |
| df_negative = df[df['sentiment_polarity'].isin(negative_sentiments)] |
|
|
| heatmap_data = pd.crosstab( |
| df[row_dimension], |
| df[col_dimension], |
| values=(df['sentiment_polarity'].isin(negative_sentiments)).astype(int), |
| aggfunc='mean' |
| ) * 100 |
|
|
| fig = go.Figure(data=go.Heatmap( |
| z=heatmap_data.values, |
| x=heatmap_data.columns, |
| y=heatmap_data.index, |
| colorscale='RdYlGn_r', |
| text=heatmap_data.values.round(1), |
| texttemplate='%{text}%', |
| textfont={"size": 12}, |
| hovertemplate='<b>%{y} - %{x}</b><br>Negative: %{z:.1f}%<extra></extra>', |
| colorbar=dict(title="Negative %") |
| )) |
|
|
| fig.update_layout( |
| title=title, |
| xaxis_title=col_dimension.capitalize(), |
| yaxis_title=row_dimension.capitalize(), |
| height=self.chart_height |
| ) |
|
|
| return fig |
|
|
| def create_sentiment_timeline(self, df, freq='D', title="Sentiment Over Time"): |
| """ |
| Create line chart showing sentiment trends over time |
| |
| Args: |
| df: Sentiment dataframe with comment_timestamp |
| freq: Frequency for aggregation ('D', 'W', 'M') |
| title: Chart title |
| |
| Returns: |
| plotly.graph_objects.Figure |
| """ |
| if 'comment_timestamp' not in df.columns: |
| return go.Figure().add_annotation( |
| text="No timestamp data available", |
| xref="paper", yref="paper", |
| x=0.5, y=0.5, showarrow=False |
| ) |
|
|
| df_temp = df.copy() |
| df_temp['date'] = pd.to_datetime(df_temp['comment_timestamp']).dt.to_period(freq).dt.to_timestamp() |
|
|
| |
| timeline_data = df_temp.groupby(['date', 'sentiment_polarity']).size().reset_index(name='count') |
|
|
| fig = go.Figure() |
|
|
| for sentiment in self.sentiment_order: |
| sentiment_data = timeline_data[timeline_data['sentiment_polarity'] == sentiment] |
| if not sentiment_data.empty: |
| fig.add_trace(go.Scatter( |
| x=sentiment_data['date'], |
| y=sentiment_data['count'], |
| name=sentiment, |
| mode='lines+markers', |
| line=dict(color=self.sentiment_colors.get(sentiment, '#CCCCCC'), width=2), |
| marker=dict(size=6), |
| hovertemplate='<b>%{x}</b><br>Count: %{y}<extra></extra>' |
| )) |
|
|
| fig.update_layout( |
| title=title, |
| xaxis_title="Date", |
| yaxis_title="Number of Comments", |
| height=self.chart_height, |
| legend=dict(title="Sentiment", orientation="v", yanchor="top", y=1, xanchor="left", x=1.02), |
| hovermode='x unified' |
| ) |
|
|
| return fig |
|
|
| def create_sentiment_score_gauge(self, avg_score, title="Overall Sentiment Score"): |
| """ |
| Create gauge chart for average sentiment score |
| |
| Args: |
| avg_score: Average sentiment score (-2 to +2) |
| title: Chart title |
| |
| Returns: |
| plotly.graph_objects.Figure |
| """ |
| |
| normalized_score = ((avg_score + 2) / 4) * 100 |
|
|
| fig = go.Figure(go.Indicator( |
| mode="gauge+number+delta", |
| value=normalized_score, |
| domain={'x': [0, 1], 'y': [0, 1]}, |
| title={'text': title, 'font': {'size': 20}}, |
| number={'suffix': '', 'font': {'size': 40}}, |
| gauge={ |
| 'axis': {'range': [0, 100], 'tickwidth': 1, 'tickcolor': "darkblue"}, |
| 'bar': {'color': "darkblue"}, |
| 'bgcolor': "white", |
| 'borderwidth': 2, |
| 'bordercolor': "gray", |
| 'steps': [ |
| {'range': [0, 20], 'color': '#D32F2F'}, |
| {'range': [20, 40], 'color': '#FF6F00'}, |
| {'range': [40, 60], 'color': '#FFB300'}, |
| {'range': [60, 80], 'color': '#7CB342'}, |
| {'range': [80, 100], 'color': '#00C851'} |
| ], |
| 'threshold': { |
| 'line': {'color': "black", 'width': 4}, |
| 'thickness': 0.75, |
| 'value': normalized_score |
| } |
| } |
| )) |
|
|
| fig.update_layout( |
| height=300, |
| margin=dict(l=20, r=20, t=60, b=20) |
| ) |
|
|
| return fig |