"""Plotlyグラフ生成モジュール."""
import logging
from typing import Optional
import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots
logger = logging.getLogger(__name__)
class ChartGenerator:
"""Plotlyグラフ生成を行うクラス.
モダンなデザインでインタラクティブなグラフを生成します。
"""
def __init__(self):
"""初期化."""
# カラーパレット (Light Theme)
self.colors = {
"primary": "#A100FF", # パープル
"secondary": "#6B00B6", # ディープパープル
"accent": "#C966FF", # ライトパープル
"background": "#FFFFFF", # ホワイト
"plot_bg": "#F8F8F8", # オフホワイト
"text": "#333333", # ダークグレー
"grid": "#E0E0E0", # ライトグレー
}
# 視認性が高く目に優しいカラフルなパレット
self.color_sequence = [
"#A100FF", # パープル
"#3B82F6", # ブルー
"#10B981", # エメラルドグリーン
"#F59E0B", # オレンジ
"#EC4899", # ピンク
"#14B8A6", # ティール
"#6366F1", # インディゴ
"#F43F5E", # ローズ
"#8B5CF6", # バイオレット
"#06B6D4", # シアン
]
def create_bar_chart(self, data: dict, title: str = "頻出キーワード", top_n: int = 20) -> str:
"""棒グラフ生成(HTML文字列).
Args:
data: キーワードと頻度の辞書
title: グラフタイトル
top_n: 上位N件表示
Returns:
str: PlotlyグラフのHTML
"""
if not data:
logger.warning("データが空のため、棒グラフを生成できません")
return ""
# 上位N件を取得
sorted_data = sorted(data.items(), key=lambda x: x[1], reverse=True)[:top_n]
keywords = [item[0] for item in sorted_data]
counts = [item[1] for item in sorted_data]
# 各バーに異なる色を割り当て(カラーパレットを循環)
bar_colors = [self.color_sequence[i % len(self.color_sequence)] for i in range(len(keywords))]
# グラフ作成
fig = go.Figure(
data=[
go.Bar(
x=counts,
y=keywords,
orientation="h",
marker=dict(color=bar_colors, line=dict(color="#FFFFFF", width=1)),
hovertemplate="<b>%{y}</b><br>出現回数: %{x}<extra></extra>",
)
]
)
# テーマ適用
self._apply_accenture_theme(fig, title)
# Y軸を逆順にして上位を上に表示(全てのカテゴリを表示)
fig.update_yaxes(
autorange="reversed",
tickmode="linear", # 全てのカテゴリを表示
dtick=1, # 1つずつ表示
)
logger.info(f"棒グラフ生成完了: {len(keywords)}件")
return fig.to_html(include_plotlyjs="cdn", div_id="bar-chart")
def create_pie_chart(self, data: dict, title: str = "分布") -> str:
"""円グラフ生成.
Args:
data: カテゴリと値の辞書
title: グラフタイトル
Returns:
str: PlotlyグラフのHTML
"""
if not data:
logger.warning("データが空のため、円グラフを生成できません")
return ""
labels = list(data.keys())
values = list(data.values())
# グラフ作成
fig = go.Figure(
data=[
go.Pie(
labels=labels,
values=values,
hole=0.4, # ドーナツチャート
marker=dict(colors=self.color_sequence, line=dict(color=self.colors["background"], width=2)),
hovertemplate="<b>%{label}</b><br>件数: %{value}<br>割合: %{percent}<extra></extra>",
)
]
)
# テーマ適用
self._apply_accenture_theme(fig, title)
# タイトルから一意のdiv_idを生成(日本語を含む場合はハッシュ化)
import hashlib
title_hash = hashlib.md5(title.encode()).hexdigest()[:8]
div_id = f"pie-chart-{title_hash}"
logger.info(f"円グラフ生成完了: {len(labels)}カテゴリ (div_id={div_id})")
return fig.to_html(include_plotlyjs="cdn", div_id=div_id)
def create_radar_chart(self, data: dict, title: str = "軸別スコア") -> str:
"""レーダーチャート生成.
Args:
data: カテゴリとスコアの辞書
title: グラフタイトル
Returns:
str: PlotlyグラフのHTML
"""
if not data:
logger.warning("データが空のため、レーダーチャートを生成できません")
return ""
categories = list(data.keys())
values = list(data.values())
# 最初の要素を最後にも追加して閉じた図形にする
categories_closed = categories + [categories[0]]
values_closed = values + [values[0]]
# グラフ作成
fig = go.Figure(
data=[
go.Scatterpolar(
r=values_closed,
theta=categories_closed,
fill="toself",
fillcolor=self.colors["primary"],
opacity=0.6,
line=dict(color=self.colors["secondary"], width=2),
hovertemplate="<b>%{theta}</b><br>スコア: %{r}<extra></extra>",
)
]
)
# テーマ適用
fig.update_layout(
polar=dict(
bgcolor=self.colors["background"],
radialaxis=dict(
visible=True, range=[0, max(values) * 1.1], gridcolor=self.colors["grid"], gridwidth=1, tickfont=dict(color=self.colors["text"])
),
angularaxis=dict(gridcolor=self.colors["grid"], gridwidth=1, tickfont=dict(color=self.colors["text"])),
),
showlegend=False,
)
self._apply_accenture_theme(fig, title)
logger.info(f"レーダーチャート生成完了: {len(categories)}軸")
return fig.to_html(include_plotlyjs="cdn", div_id="radar-chart")
def create_heatmap(self, df: pd.DataFrame, title: str = "ヒートマップ") -> str:
"""ヒートマップ生成.
Args:
df: DataFrame(行:カテゴリ、列:キーワード)
title: グラフタイトル
Returns:
str: PlotlyグラフのHTML
"""
if df.empty:
logger.warning("データが空のため、ヒートマップを生成できません")
return ""
# グラフ作成
fig = go.Figure(
data=[
go.Heatmap(
z=df.values,
x=df.columns.tolist(),
y=df.index.tolist(),
colorscale=[[0, self.colors["background"]], [0.5, self.colors["secondary"]], [1, self.colors["primary"]]],
hovertemplate="<b>%{y}</b><br><b>%{x}</b><br>出現回数: %{z}<extra></extra>",
)
]
)
# テーマ適用
self._apply_accenture_theme(fig, title)
logger.info(f"ヒートマップ生成完了: {df.shape[0]}行 x {df.shape[1]}列")
return fig.to_html(include_plotlyjs="cdn", div_id="heatmap")
def _apply_accenture_theme(self, fig: go.Figure, title: str) -> None:
"""モダンテーマ適用 (Light Theme).
Args:
fig: Plotly Figure
title: グラフタイトル
"""
fig.update_layout(
title=dict(
text=title,
font=dict(size=24, color=self.colors["primary"], family="Montserrat, sans-serif", weight=700),
x=0.5,
xanchor="center",
),
paper_bgcolor=self.colors["background"],
plot_bgcolor=self.colors["plot_bg"],
font=dict(color=self.colors["text"], family="Noto Sans JP, sans-serif"),
hoverlabel=dict(bgcolor=self.colors["primary"], font_size=14, font_family="Noto Sans JP, sans-serif", font_color="white"),
margin=dict(l=80, r=80, t=100, b=80),
xaxis=dict(gridcolor=self.colors["grid"], gridwidth=1, showline=True, linecolor=self.colors["grid"]),
yaxis=dict(gridcolor=self.colors["grid"], gridwidth=1, showline=True, linecolor=self.colors["grid"]),
)