| """ |
| Demographic visualization charts for sentiment analysis |
| Handles age, timezone, and experience level visualizations |
| """ |
| import plotly.graph_objects as go |
| import plotly.express as px |
| import json |
| from pathlib import Path |
|
|
|
|
| class DemographicCharts: |
| """ |
| Creates demographic-related visualizations for musora_app data |
| """ |
|
|
| def __init__(self): |
| """Initialize with configuration""" |
| 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_age_distribution_chart(self, age_dist_df, title="Age Distribution"): |
| """ |
| Create bar chart for age group distribution |
| |
| Args: |
| age_dist_df: DataFrame with age_group, count, percentage columns |
| title: Chart title |
| |
| Returns: |
| plotly.graph_objects.Figure |
| """ |
| if age_dist_df.empty: |
| return self._create_empty_chart(title, "No demographic data available") |
|
|
| |
| age_order = ['18-24', '25-34', '35-44', '45-54', '55+'] |
|
|
| |
| age_dist_df['age_group'] = pd.Categorical( |
| age_dist_df['age_group'], |
| categories=age_order, |
| ordered=True |
| ) |
| age_dist_df = age_dist_df.sort_values('age_group') |
|
|
| fig = go.Figure() |
|
|
| fig.add_trace(go.Bar( |
| x=age_dist_df['age_group'], |
| y=age_dist_df['count'], |
| text=age_dist_df.apply(lambda row: f"{row['count']}<br>({row['percentage']:.1f}%)", axis=1), |
| textposition='auto', |
| marker=dict( |
| color='#4A90E2', |
| line=dict(color='#2E5C8A', width=1) |
| ), |
| hovertemplate='<b>%{x}</b><br>Comments: %{y}<br>Percentage: %{customdata:.1f}%<extra></extra>', |
| customdata=age_dist_df['percentage'] |
| )) |
|
|
| fig.update_layout( |
| title=title, |
| xaxis_title="Age Group", |
| yaxis_title="Number of Comments", |
| height=self.chart_height, |
| showlegend=False, |
| hovermode='x' |
| ) |
|
|
| return fig |
|
|
| def create_age_sentiment_chart(self, age_sentiment_df, title="Sentiment by Age Group"): |
| """ |
| Create stacked bar chart showing sentiment distribution for each age group |
| |
| Args: |
| age_sentiment_df: DataFrame with age_group, sentiment_polarity, count, percentage |
| title: Chart title |
| |
| Returns: |
| plotly.graph_objects.Figure |
| """ |
| if age_sentiment_df.empty: |
| return self._create_empty_chart(title, "No demographic data available") |
|
|
| |
| age_order = ['18-24', '25-34', '35-44', '45-54', '55+'] |
|
|
| fig = go.Figure() |
|
|
| |
| for sentiment in self.sentiment_order: |
| sentiment_data = age_sentiment_df[age_sentiment_df['sentiment_polarity'] == sentiment] |
|
|
| if not sentiment_data.empty: |
| fig.add_trace(go.Bar( |
| name=sentiment.replace('_', ' ').title(), |
| x=sentiment_data['age_group'], |
| y=sentiment_data['percentage'], |
| marker=dict(color=self.sentiment_colors.get(sentiment, '#999999')), |
| hovertemplate='<b>%{fullData.name}</b><br>Age: %{x}<br>Percentage: %{y:.1f}%<extra></extra>' |
| )) |
|
|
| fig.update_layout( |
| title=title, |
| xaxis=dict( |
| title="Age Group", |
| categoryorder='array', |
| categoryarray=age_order |
| ), |
| yaxis=dict( |
| title="Percentage (%)", |
| range=[0, 100] |
| ), |
| barmode='stack', |
| height=self.chart_height, |
| hovermode='x unified', |
| legend=dict( |
| orientation="h", |
| yanchor="bottom", |
| y=1.02, |
| xanchor="right", |
| x=1 |
| ) |
| ) |
|
|
| return fig |
|
|
| def create_timezone_chart(self, timezone_df, title="Top Timezones", top_n=15): |
| """ |
| Create horizontal bar chart for top timezones |
| |
| Args: |
| timezone_df: DataFrame with timezone, count, percentage columns |
| title: Chart title |
| top_n: Number of top timezones to display |
| |
| Returns: |
| plotly.graph_objects.Figure |
| """ |
| if timezone_df.empty: |
| return self._create_empty_chart(title, "No timezone data available") |
|
|
| |
| display_df = timezone_df.head(top_n).iloc[::-1] |
|
|
| fig = go.Figure() |
|
|
| fig.add_trace(go.Bar( |
| y=display_df['timezone'], |
| x=display_df['count'], |
| orientation='h', |
| text=display_df.apply(lambda row: f"{row['count']} ({row['percentage']:.1f}%)", axis=1), |
| textposition='auto', |
| marker=dict( |
| color='#50C878', |
| line=dict(color='#2E7D4E', width=1) |
| ), |
| hovertemplate='<b>%{y}</b><br>Comments: %{x}<br>Percentage: %{customdata:.1f}%<extra></extra>', |
| customdata=display_df['percentage'] |
| )) |
|
|
| fig.update_layout( |
| title=title, |
| xaxis_title="Number of Comments", |
| yaxis_title="Timezone", |
| height=max(self.chart_height, top_n * 25), |
| showlegend=False, |
| hovermode='y' |
| ) |
|
|
| return fig |
|
|
| def create_region_distribution_chart(self, region_df, title="Distribution by Region"): |
| """ |
| Create pie chart for timezone region distribution |
| |
| Args: |
| region_df: DataFrame with timezone_region, count, percentage columns |
| title: Chart title |
| |
| Returns: |
| plotly.graph_objects.Figure |
| """ |
| if region_df.empty: |
| return self._create_empty_chart(title, "No region data available") |
|
|
| |
| colors = px.colors.qualitative.Set3 |
|
|
| fig = go.Figure() |
|
|
| fig.add_trace(go.Pie( |
| labels=region_df['timezone_region'], |
| values=region_df['count'], |
| textinfo='label+percent', |
| hovertemplate='<b>%{label}</b><br>Comments: %{value}<br>Percentage: %{percent}<extra></extra>', |
| marker=dict(colors=colors) |
| )) |
|
|
| fig.update_layout( |
| title=title, |
| height=self.chart_height, |
| showlegend=True, |
| legend=dict( |
| orientation="v", |
| yanchor="middle", |
| y=0.5, |
| xanchor="left", |
| x=1 |
| ) |
| ) |
|
|
| return fig |
|
|
| def create_region_sentiment_chart(self, region_sentiment_df, title="Sentiment by Region"): |
| """ |
| Create grouped bar chart showing sentiment distribution for each region |
| |
| Args: |
| region_sentiment_df: DataFrame with timezone_region, sentiment_polarity, count, percentage |
| title: Chart title |
| |
| Returns: |
| plotly.graph_objects.Figure |
| """ |
| if region_sentiment_df.empty: |
| return self._create_empty_chart(title, "No region sentiment data available") |
|
|
| fig = go.Figure() |
|
|
| |
| for sentiment in self.sentiment_order: |
| sentiment_data = region_sentiment_df[region_sentiment_df['sentiment_polarity'] == sentiment] |
|
|
| if not sentiment_data.empty: |
| fig.add_trace(go.Bar( |
| name=sentiment.replace('_', ' ').title(), |
| x=sentiment_data['timezone_region'], |
| y=sentiment_data['percentage'], |
| marker=dict(color=self.sentiment_colors.get(sentiment, '#999999')), |
| hovertemplate='<b>%{fullData.name}</b><br>Region: %{x}<br>Percentage: %{y:.1f}%<extra></extra>' |
| )) |
|
|
| fig.update_layout( |
| title=title, |
| xaxis_title="Region", |
| yaxis=dict( |
| title="Percentage (%)", |
| range=[0, 100] |
| ), |
| barmode='stack', |
| height=self.chart_height, |
| hovermode='x unified', |
| legend=dict( |
| orientation="h", |
| yanchor="bottom", |
| y=1.02, |
| xanchor="right", |
| x=1 |
| ) |
| ) |
|
|
| return fig |
|
|
| def create_experience_distribution_chart(self, exp_df, title="Experience Level Distribution", use_groups=False): |
| """ |
| Create bar chart for experience level distribution |
| |
| Args: |
| exp_df: DataFrame with experience_level/experience_group, count, percentage columns |
| title: Chart title |
| use_groups: If True, display grouped experience levels |
| |
| Returns: |
| plotly.graph_objects.Figure |
| """ |
| if exp_df.empty: |
| return self._create_empty_chart(title, "No experience data available") |
|
|
| field = 'experience_group' if use_groups else 'experience_level' |
|
|
| |
| if use_groups: |
| exp_order = ['Beginner (0-3)', 'Intermediate (4-7)', 'Advanced (8-10)'] |
| exp_df[field] = pd.Categorical( |
| exp_df[field], |
| categories=exp_order, |
| ordered=True |
| ) |
| exp_df = exp_df.sort_values(field) |
| else: |
| |
| exp_df = exp_df.sort_values(field) |
|
|
| fig = go.Figure() |
|
|
| fig.add_trace(go.Bar( |
| x=exp_df[field], |
| y=exp_df['count'], |
| text=exp_df.apply(lambda row: f"{row['count']}<br>({row['percentage']:.1f}%)", axis=1), |
| textposition='auto', |
| marker=dict( |
| color='#9B59B6', |
| line=dict(color='#6C3483', width=1) |
| ), |
| hovertemplate='<b>%{x}</b><br>Comments: %{y}<br>Percentage: %{customdata:.1f}%<extra></extra>', |
| customdata=exp_df['percentage'] |
| )) |
|
|
| fig.update_layout( |
| title=title, |
| xaxis_title="Experience Level" if not use_groups else "Experience Group", |
| yaxis_title="Number of Comments", |
| height=self.chart_height, |
| showlegend=False, |
| hovermode='x' |
| ) |
|
|
| return fig |
|
|
| def create_experience_sentiment_chart(self, exp_sentiment_df, title="Sentiment by Experience Level", use_groups=False): |
| """ |
| Create stacked bar chart showing sentiment distribution for each experience level |
| |
| Args: |
| exp_sentiment_df: DataFrame with experience_level/experience_group, sentiment_polarity, count, percentage |
| title: Chart title |
| use_groups: If True, use grouped experience levels |
| |
| Returns: |
| plotly.graph_objects.Figure |
| """ |
| if exp_sentiment_df.empty: |
| return self._create_empty_chart(title, "No experience sentiment data available") |
|
|
| field = 'experience_group' if use_groups else 'experience_level' |
|
|
| fig = go.Figure() |
|
|
| |
| for sentiment in self.sentiment_order: |
| sentiment_data = exp_sentiment_df[exp_sentiment_df['sentiment_polarity'] == sentiment] |
|
|
| if not sentiment_data.empty: |
| fig.add_trace(go.Bar( |
| name=sentiment.replace('_', ' ').title(), |
| x=sentiment_data[field], |
| y=sentiment_data['percentage'], |
| marker=dict(color=self.sentiment_colors.get(sentiment, '#999999')), |
| hovertemplate='<b>%{fullData.name}</b><br>Experience: %{x}<br>Percentage: %{y:.1f}%<extra></extra>' |
| )) |
|
|
| |
| if use_groups: |
| exp_order = ['Beginner (0-3)', 'Intermediate (4-7)', 'Advanced (8-10)'] |
| xaxis_config = dict( |
| title="Experience Group", |
| categoryorder='array', |
| categoryarray=exp_order |
| ) |
| else: |
| xaxis_config = dict(title="Experience Level") |
|
|
| fig.update_layout( |
| title=title, |
| xaxis=xaxis_config, |
| yaxis=dict( |
| title="Percentage (%)", |
| range=[0, 100] |
| ), |
| barmode='stack', |
| height=self.chart_height, |
| hovermode='x unified', |
| legend=dict( |
| orientation="h", |
| yanchor="bottom", |
| y=1.02, |
| xanchor="right", |
| x=1 |
| ) |
| ) |
|
|
| return fig |
|
|
| def create_demographics_heatmap(self, df, row_field, col_field, title="Demographics Heatmap"): |
| """ |
| Create heatmap for cross-demographic analysis |
| |
| Args: |
| df: DataFrame with demographic fields and sentiment |
| row_field: Field for rows (e.g., 'age_group') |
| col_field: Field for columns (e.g., 'experience_group') |
| title: Chart title |
| |
| Returns: |
| plotly.graph_objects.Figure |
| """ |
| if df.empty: |
| return self._create_empty_chart(title, "No data available for heatmap") |
|
|
| |
| pivot = df.pivot_table( |
| index=row_field, |
| columns=col_field, |
| values='count', |
| aggfunc='sum', |
| fill_value=0 |
| ) |
|
|
| fig = go.Figure(data=go.Heatmap( |
| z=pivot.values, |
| x=pivot.columns, |
| y=pivot.index, |
| colorscale='Blues', |
| text=pivot.values, |
| texttemplate='%{text}', |
| textfont={"size": 10}, |
| hovertemplate='<b>%{y}</b> × <b>%{x}</b><br>Comments: %{z}<extra></extra>' |
| )) |
|
|
| fig.update_layout( |
| title=title, |
| xaxis_title=col_field.replace('_', ' ').title(), |
| yaxis_title=row_field.replace('_', ' ').title(), |
| height=self.chart_height |
| ) |
|
|
| return fig |
|
|
| def _create_empty_chart(self, title, message): |
| """ |
| Create an empty chart with a message |
| |
| Args: |
| title: Chart title |
| message: Message to display |
| |
| Returns: |
| plotly.graph_objects.Figure |
| """ |
| fig = go.Figure() |
|
|
| fig.add_annotation( |
| text=message, |
| xref="paper", |
| yref="paper", |
| x=0.5, |
| y=0.5, |
| showarrow=False, |
| font=dict(size=14, color="gray") |
| ) |
|
|
| fig.update_layout( |
| title=title, |
| height=self.chart_height, |
| xaxis=dict(visible=False), |
| yaxis=dict(visible=False) |
| ) |
|
|
| return fig |
|
|
|
|
| |
| import pandas as pd |
|
|