Untitled
3 hours ago in Plain Text
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>幸町1丁目西丁会役員制度 アンケート分析</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;500;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Noto Sans JP', sans-serif; background-color: #f1f5f9; scroll-behavior: smooth; }
.chart-container { position: relative; width: 100%; height: 280px; }
.sidebar-chart-container { position: relative; width: 100%; height: 200px; }
.cross-chart-container { position: relative; width: 100%; height: 400px; }
.keyword-tag { transition: all 0.2s ease-in-out; }
.keyword-tag:hover { transform: translateY(-2px); box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); }
.content-section { opacity: 0; transform: translateY(20px); transition: opacity 0.6s ease-out, transform 0.6s ease-out; }
.content-section.is-visible { opacity: 1; transform: translateY(0); }
</style>
</head>
<body class="text-slate-800">
<div class="container mx-auto max-w-screen-2xl p-4 sm:p-6 lg:p-8">
<header class="text-center mb-10">
<h1 class="text-3xl md:text-4xl font-bold text-slate-800">幸町1丁目西丁会役員制度</h1>
<p class="text-lg text-slate-500 mt-2">アンケート インタラクティブ分析</p>
</header>
<div class="lg:grid lg:grid-cols-12 lg:gap-8">
<aside class="lg:col-span-3 lg:sticky top-8 self-start">
<div class="space-y-6 bg-white p-6 rounded-xl shadow-lg">
<div>
<h3 class="font-bold text-slate-700">全体集計</h3>
<div class="mt-4 flex items-center justify-between bg-slate-50 p-3 rounded-lg">
<span class="text-sm font-medium text-slate-600">総回答数</span>
<span id="totalResponses" class="text-2xl font-bold text-sky-600"></span>
</div>
</div>
<div>
<label for="hanFilter" class="block text-sm font-medium text-slate-700 mb-2 font-bold">班で絞り込み</label>
<select id="hanFilter" class="w-full rounded-md border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500">
<option value="all">すべての班</option>
</select>
</div>
<div>
<h3 class="font-bold text-slate-700 mb-2">自由意見の傾向</h3>
<div class="sidebar-chart-container">
<canvas id="sentimentChart"></canvas>
</div>
</div>
</div>
</aside>
<main class="lg:col-span-9 mt-8 lg:mt-0">
<div class="space-y-12">
<section class="content-section bg-white p-6 rounded-xl shadow-lg">
<h2 class="text-2xl font-bold text-slate-800 mb-6">分析サマリー</h2>
<div class="space-y-6">
<div>
<h3 class="font-semibold text-lg text-slate-700 mb-4">分析から見えたこと</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="bg-sky-50 p-6 rounded-lg text-center">
<div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-sky-100">
<svg class="h-6 w-6 text-sky-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<p class="mt-4 text-3xl font-bold text-sky-600">93.1%</p>
<p class="text-sm font-medium text-sky-800 mt-1">が「任期1年」に賛成</p>
<p class="text-xs text-sky-700 mt-2">改正の基本方針は広く受け入れられています。</p>
</div>
<div class="bg-amber-50 p-6 rounded-lg text-center">
<div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-amber-100">
<svg class="h-6 w-6 text-amber-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
</svg>
</div>
<p class="mt-4 text-3xl font-bold text-amber-600">約2割</p>
<p class="text-sm font-medium text-amber-800 mt-1">が「輪番制」に反対・その他</p>
<p class="text-xs text-amber-700 mt-2">任期1年賛成者の中にも懸念があり、複雑な意向が伺えます。</p>
</div>
<div class="bg-emerald-50 p-6 rounded-lg text-center">
<div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-emerald-100">
<svg class="h-6 w-6 text-emerald-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
</svg>
</div>
<p class="mt-4 text-lg font-bold text-emerald-800">運用の具体化</p>
<p class="text-sm font-medium text-emerald-800 mt-1">が成功の鍵</p>
<p class="text-xs text-emerald-700 mt-2">意見は「配慮」「役職の適性」「引継ぎ」に集中しています。</p>
</div>
</div>
</div>
<div class="mt-4 bg-slate-100 p-6 rounded-lg">
<h3 class="font-semibold text-lg text-slate-700 mb-2">結論と次のステップ</h3>
<p class="text-sm text-slate-600">制度改正の大きな流れは支持されています。今後は、輪番制の**「例外規定」**や**「対象役職」**など、具体的な運用ルールを整備し、住民の不安に応えることが最重要課題となります。</p>
</div>
</div>
</section>
<section class="content-section bg-white p-6 rounded-xl shadow-lg">
<h2 class="text-2xl font-bold text-slate-800 mb-6">各設問の賛否</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<div class="text-center">
<h3 class="font-semibold text-slate-700">問1:任期1年</h3>
<div class="chart-container mt-2">
<canvas id="q1Chart"></canvas>
</div>
</div>
<div class="text-center">
<h3 class="font-semibold text-slate-700">問2:選出方法</h3>
<div class="chart-container mt-2">
<canvas id="q2Chart"></canvas>
</div>
</div>
<div class="text-center">
<h3 class="font-semibold text-slate-700">問3:輪番制</h3>
<div class="chart-container mt-2">
<canvas id="q3Chart"></canvas>
</div>
</div>
</div>
</section>
<section class="content-section bg-white p-6 rounded-xl shadow-lg">
<h2 class="text-2xl font-bold text-slate-800 mb-2">回答の関連性を探る</h2>
<p class="text-slate-500 mb-6">ある質問の回答者が、別の質問にどう答えたかを探ります。グラフの棒をクリックすると、下の「住民の声」が該当意見に絞り込まれます。</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div>
<label for="crossAxis" class="block text-sm font-medium text-slate-700">分析の軸:</label>
<select id="crossAxis" class="mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500">
<option value="q1">問1:任期1年</option>
<option value="q2">問2:選出方法</option>
<option value="q3">問3:輪番制</option>
</select>
</div>
<div>
<label for="crossCompare" class="block text-sm font-medium text-slate-700">比較項目:</label>
<select id="crossCompare" class="mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500">
<option value="q1" disabled>問1:任期1年</option>
<option value="q2">問2:選出方法</option>
<option value="q3" selected>問3:輪番制</option>
</select>
</div>
</div>
<div class="cross-chart-container mt-8">
<canvas id="crossChart"></canvas>
</div>
</section>
<section id="opinion-explorer" class="content-section bg-white p-6 rounded-xl shadow-lg">
<h2 class="text-2xl font-bold text-slate-800 mb-2">住民の声を探る</h2>
<p id="opinion-explorer-description" class="text-slate-500 mb-6">自由意見の一覧です。上のグラフや下のキーワードタグで絞り込めます。</p>
<div id="keyword-tags" class="flex flex-wrap gap-2 mb-6 border-b border-slate-200 pb-6"></div>
<div>
<div class="flex justify-between items-center mb-4">
<h3 class="font-bold text-slate-700">表示中の意見 (<span id="comment-count">0</span>件)</h3>
<button id="clear-filters" class="text-sm text-sky-600 hover:text-sky-800 font-semibold hidden">フィルターを解除</button>
</div>
<div id="comment-list" class="space-y-3 max-h-[400px] overflow-y-auto pr-2"></div>
</div>
</section>
</div>
</main>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const rawData = [
{ han: "1", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "1", q1: "反対", q2: "反対", q3: "反対", q1_other: "", q2_other: "", q3_other: "" },
{ han: "1", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "1", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "1", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "1", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "1", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "2", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "2", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "2", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "2", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "2", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "2", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "2", q1: "反対", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "3", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "3", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "3", q1: "賛成", q2: "反対", q3: "賛成", q1_other: "", q2_other: "", q3_other: "輪番制を導入する前に役員により人選してみて下さい。それでも決まらないなら、その後に輪番制を導入してみて下さい。" },
{ han: "4", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "4", q1: "賛成", q2: "反対", q3: "反対", q1_other: "", q2_other: "", q3_other: "平日の日昼に時間を作りやすい方々で優先的にお願いしたい。" },
{ han: "4", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "4", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "4", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "4", q1: "賛成", q2: "賛成", q3: "反対", q1_other: "", q2_other: "", q3_other: "" },
{ han: "4", q1: "賛成", q2: "賛成", q3: "反対", q1_other: "", q2_other: "", q3_other: "" },
{ han: "4", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "4", q1: "賛成", q2: "賛成", q3: "反対", q1_other: "", q2_other: "", q3_other: "" },
{ han: "5", q1: "賛成", q2: "賛成", q3: "反対", q1_other: "", q2_other: "", q3_other: "" },
{ han: "5", q1: "賛成", q2: "賛成", q3: "反対", q1_other: "", q2_other: "", q3_other: "" },
{ han: "5", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "5", q1: "その他", q2: "賛成", q3: "反対", q1_other: "会則を次のように変更し役員の任期を1年としたらどうか?「役員の任期は、1年とし、2年まで延長することが出来る。」", q2_other: "", q3_other: "従来どおりでよいと思う。ただし、身体等特別の理由のある者を除く。" },
{ han: "5", q1: "賛成", q2: "賛成", q3: "反対", q1_other: "", q2_other: "", q3_other: "" },
{ han: "5", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "5", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "6", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "6", q1: "賛成", q2: "その他", q3: "賛成", q1_other: "", q2_other: "原則、賛成ですが、小規模班については対応について、検討が必要と思います。", q3_other: "" },
{ han: "6", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "6", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "7", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "8", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "8", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "8", q1: "その他", q2: "無回答", q3: "無回答", q1_other: "次回、班長が回ってくる頃の年齢を考えると、質問に答える資格がないと思います。", q2_other: "", q3_other: "" },
{ han: "9", q1: "賛成", q2: "賛成", q3: "その他", q1_other: "", q2_other: "", q3_other: "次回、班長になるころには、班長スキップの側にいる者としては、アンケートに回答する事に少々うしろめたさを感じています。" },
{ han: "9", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "9", q1: "賛成", q2: "反対", q3: "反対", q1_other: "", q2_other: "", q3_other: "" },
{ han: "9", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "9", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "9", q1: "その他", q2: "その他", q3: "その他", q1_other: "", q2_other: "", q3_other: "" },
{ han: "9", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "9", q1: "賛成", q2: "賛成", q3: "反対", q1_other: "", q2_other: "", q3_other: "" },
{ han: "10", q1: "その他", q2: "その他", q3: "その他", q1_other: "賛成ではありますが、高齢の為対応が不可能", q2_other: "賛成ではありますが、高齢の為対応が不可能", q3_other: "賛成ではありますが、高齢の為対応が不可能" },
{ han: "10", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "10", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "10", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "10", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
{ han: "10", q1: "賛成", q2: "その他", q3: "その他", q1_other: "", q2_other: "理事、会計、監事については、新班長内からの選出でよいが、丁会長については、除外したほうが良い。丁会長の仕事の内容、ボリューム、責任を考えるとだれでもできるわけではないので。", q3_other: "2と同じで、丁会長については、輪番でも良いが、会計はできないという方も出るかもしれないです。" },
{ han: "10", q1: "賛成", q2: "賛成", q3: "賛成", q1_other: "", q2_other: "", q3_other: "" },
];
const chartInstances = {};
const questionMap = { q1: "問1:任期", q2: "問2:選出", q3: "問3:輪番制" };
const answerOptions = ['賛成', '反対', 'その他'];
const colors = { '賛成': 'rgb(56, 189, 248)', '反対': 'rgb(251, 146, 60)', 'その他': 'rgb(161, 161, 170)' };
const sentimentColors = { '賛成的': '#10b981', '懸念・批判的': '#f43f5e', '中立': '#64748b' };
const keywords = ['負担', '高齢者', '引継ぎ', '公平', '経験', '時間', '強制', '協力', '配慮', '明確', '子供', '任期', '責任', '会長', '事情', '理由'];
const negKeywords = ['反対', 'ない', '難しい', '不安', '懸念', '強制', '問題', '短すぎる', '大きい', '不可能'];
const posKeywords = ['良い', '賛成', '協力', '必要', '公平', '効率的', 'お願いしたい'];
let activeKeyword = 'all';
let drilldownFilter = null;
const getSentiment = (text) => {
if (negKeywords.some(kw => text.includes(kw))) return '懸念・批判的';
if (posKeywords.some(kw => text.includes(kw))) return '賛成的';
return '中立';
};
const createDoughnutChart = (ctx, data, labels) => {
const bgColors = labels.map(l => colors[l] || sentimentColors[l] || '#cbd5e1');
return new Chart(ctx, { type: 'doughnut', data: { labels: labels, datasets: [{ data: data, backgroundColor: bgColors, borderColor: '#ffffff', borderWidth: 4 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { padding: 15, boxWidth: 12 } }, tooltip: { callbacks: { label: c => `${c.label}: ${c.raw}件 (${(c.raw / c.chart.getDatasetMeta(0).total * 100).toFixed(1)}%)` } } }, cutout: '60%' } });
};
const createBarChart = (ctx) => {
return new Chart(ctx, { type: 'bar', data: { labels: [], datasets: [] }, options: { scales: { x: { stacked: true, grid: { display: false } }, y: { stacked: true, beginAtZero: true } }, responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'top' } }, onClick: handleChartClick }});
}
const updateDashboard = (filter = 'all') => {
const filteredData = filter === 'all' ? rawData : rawData.filter(d => d.han === filter);
document.getElementById('totalResponses').textContent = filteredData.length;
['q1', 'q2', 'q3'].forEach(qKey => {
const counts = { '賛成': 0, '反対': 0, 'その他': 0, '無回答': 0 };
filteredData.forEach(row => { if(row[qKey]) counts[row[qKey]]++ });
const chartData = [counts['賛成'], counts['反対'], counts['その他']];
const chartId = `${qKey}Chart`;
if (chartInstances[chartId]) chartInstances[chartId].destroy();
chartInstances[chartId] = createDoughnutChart(document.getElementById(chartId).getContext('2d'), chartData, answerOptions);
});
updateSentimentChart(filteredData);
updateCrossAnalysis();
updateOpinionExplorer();
};
const updateCrossAnalysis = () => {
const filter = document.getElementById('hanFilter').value;
const filteredData = filter === 'all' ? rawData : rawData.filter(d => d.han === filter);
const axisKey = document.getElementById('crossAxis').value;
const compareKey = document.getElementById('crossCompare').value;
const results = {};
answerOptions.forEach(opt => { results[opt] = {'賛成': 0, '反対': 0, 'その他': 0}; });
filteredData.forEach(row => { if (row[axisKey] && row[compareKey] && row[axisKey] !== '無回答' && row[compareKey] !== '無回答') results[row[axisKey]][row[compareKey]]++; });
const axisShortName = (questionMap[axisKey].split(':')[1] || questionMap[axisKey]).replace('1年','');
const compareShortName = (questionMap[compareKey].split(':')[1] || questionMap[compareKey]).replace('1年','');
const chartData = {
labels: answerOptions.map(opt => `${axisShortName}「${opt}」`),
datasets: answerOptions.map(opt => ({
label: `${compareShortName}「${opt}」`,
data: answerOptions.map(axisOpt => results[axisOpt][opt]),
backgroundColor: colors[opt],
axisKey: axisKey,
compareKey: compareKey,
compareValue: opt
}))
};
if (!chartInstances.crossChart) {
chartInstances.crossChart = createBarChart(document.getElementById('crossChart').getContext('2d'));
}
chartInstances.crossChart.data = chartData;
chartInstances.crossChart.update();
};
const updateSentimentChart = (data) => {
const sentimentCounts = { '賛成的': 0, '懸念・批判的': 0, '中立': 0 };
['q1_other', 'q2_other', 'q3_other'].forEach(key => {
data.forEach(row => {
if (row[key] && row[key].trim() !== "") {
sentimentCounts[getSentiment(row[key])]++;
}
});
});
const chartData = [sentimentCounts['賛成的'], sentimentCounts['懸念・批判的'], sentimentCounts['中立']];
const labels = ['賛成的', '懸念・批判的', '中立'];
if (chartInstances.sentimentChart) chartInstances.sentimentChart.destroy();
chartInstances.sentimentChart = createDoughnutChart(document.getElementById('sentimentChart').getContext('2d'), chartData, labels);
};
const updateOpinionExplorer = () => {
const hanFilterVal = document.getElementById('hanFilter').value;
const explorerDesc = document.getElementById('opinion-explorer-description');
const clearFiltersBtn = document.getElementById('clear-filters');
let dataPool = hanFilterVal === 'all' ? rawData : rawData.filter(d => d.han === hanFilterVal);
let allComments = [];
['q1_other', 'q2_other', 'q3_other'].forEach(key => {
const qKey = key.substring(0, 2);
dataPool.forEach(row => {
if (row[key] && row[key].trim() !== "") {
allComments.push({ text: row[key], han: row.han, q: questionMap[qKey], sentiment: getSentiment(row[key]), original: row });
}
});
});
if (drilldownFilter) {
allComments = allComments.filter(c => c.original[drilldownFilter.axisKey] === drilldownFilter.axisValue && c.original[drilldownFilter.compareKey] === drilldownFilter.compareValue);
const axisShortName = (questionMap[drilldownFilter.axisKey].split(':')[1] || questionMap[drilldownFilter.axisKey]).replace('1年','');
const compareShortName = (questionMap[drilldownFilter.compareKey].split(':')[1] || questionMap[drilldownFilter.compareKey]).replace('1年','');
explorerDesc.innerHTML = `<span class="font-bold text-sky-600">${axisShortName}「${drilldownFilter.axisValue}」かつ ${compareShortName}「${drilldownFilter.compareValue}」</span>と回答した人の意見です。`;
clearFiltersBtn.classList.remove('hidden');
} else {
explorerDesc.textContent = '自由意見の一覧です。上のグラフや下のキーワードタグで絞り込めます。';
clearFiltersBtn.classList.add('hidden');
}
updateKeywordTags(allComments);
const commentsToShow = activeKeyword === 'all' ? allComments : allComments.filter(c => c.text.includes(activeKeyword));
document.getElementById('comment-count').textContent = commentsToShow.length;
const commentList = document.getElementById('comment-list');
if (commentsToShow.length > 0) {
commentList.innerHTML = commentsToShow.map(c => `
<div class="bg-slate-50 p-4 rounded-lg border-l-4" style="border-color: ${sentimentColors[c.sentiment]}">
<p class="text-slate-800">${c.text}</p>
<p class="text-xs text-slate-500 mt-2">[班: ${c.han}] - [${c.q}]</p>
</div>
`).join('');
} else {
commentList.innerHTML = `<p class="text-slate-500 p-4 text-center">該当する意見はありません。</p>`;
}
};
const updateKeywordTags = (comments) => {
const tagsContainer = document.getElementById('keyword-tags');
const keywordCounts = {};
keywords.forEach(kw => keywordCounts[kw] = 0);
comments.forEach(comment => {
keywords.forEach(kw => {
if (comment.text.includes(kw)) keywordCounts[kw]++;
});
});
tagsContainer.innerHTML = '';
const allTag = document.createElement('button');
allTag.textContent = `すべての意見 (${comments.length})`;
allTag.dataset.keyword = 'all';
allTag.className = `keyword-tag text-sm font-semibold py-1 px-3 rounded-full cursor-pointer ${activeKeyword === 'all' ? 'bg-sky-500 text-white shadow' : 'bg-slate-200 text-slate-700'}`;
tagsContainer.appendChild(allTag);
Object.entries(keywordCounts).filter(([, count]) => count > 0).sort((a, b) => b[1] - a[1]).forEach(([kw, count]) => {
const tag = document.createElement('button');
tag.textContent = `${kw} (${count})`;
tag.dataset.keyword = kw;
tag.className = `keyword-tag text-sm font-semibold py-1 px-3 rounded-full cursor-pointer ${activeKeyword === kw ? 'bg-sky-500 text-white shadow' : 'bg-slate-200 text-slate-700'}`;
tagsContainer.appendChild(tag);
});
}
function handleChartClick(event) {
const chart = chartInstances.crossChart;
const points = chart.getElementsAtEventForMode(event, 'nearest', { intersect: true }, true);
if (points.length) {
const firstPoint = points[0];
const dataset = chart.data.datasets[firstPoint.datasetIndex];
const axisValue = chart.data.labels[firstPoint.index].split('「')[1].replace('」','');
drilldownFilter = {
axisKey: dataset.axisKey,
axisValue: axisValue,
compareKey: dataset.compareKey,
compareValue: dataset.compareValue
};
activeKeyword = 'all';
updateOpinionExplorer();
document.getElementById('opinion-explorer').scrollIntoView({ behavior: 'smooth' });
}
}
const setupControls = () => {
const hanFilter = document.getElementById('hanFilter');
const crossAxis = document.getElementById('crossAxis');
const crossCompare = document.getElementById('crossCompare');
const uniqueHans = [...new Set(rawData.map(item => item.han))].sort((a,b) => a - b);
uniqueHans.forEach(han => {
const option = document.createElement('option');
option.value = han;
option.textContent = `${han}班`;
hanFilter.appendChild(option);
});
const syncCrossSelects = () => {
const axisVal = crossAxis.value;
const compareVal = crossCompare.value;
if(axisVal === compareVal) {
for(let i = 0; i < crossCompare.options.length; i++) {
if(crossCompare.options[i].value !== axisVal) {
crossCompare.value = crossCompare.options[i].value;
break;
}
}
}
Array.from(crossCompare.options).forEach(opt => opt.disabled = (opt.value === crossAxis.value));
Array.from(crossAxis.options).forEach(opt => opt.disabled = (opt.value === crossCompare.value));
updateCrossAnalysis();
};
hanFilter.addEventListener('change', () => {
drilldownFilter = null;
activeKeyword = 'all';
updateDashboard(hanFilter.value);
});
crossAxis.addEventListener('change', syncCrossSelects);
crossCompare.addEventListener('change', syncCrossSelects);
document.getElementById('keyword-tags').addEventListener('click', (e) => {
if (e.target.tagName === 'BUTTON') {
drilldownFilter = null;
activeKeyword = e.target.dataset.keyword;
updateOpinionExplorer();
}
});
document.getElementById('clear-filters').addEventListener('click', () => {
drilldownFilter = null;
activeKeyword = 'all';
updateDashboard(hanFilter.value);
});
const sections = document.querySelectorAll('.content-section');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
observer.unobserve(entry.target);
}
});
}, { threshold: 0.1 });
sections.forEach(section => observer.observe(section));
syncCrossSelects();
};
setupControls();
updateDashboard();
});
</script>
</body>
</html>