| | """ |
| | 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 |