aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/components/SanpiTable.astro
diff options
context:
space:
mode:
authorYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-10 22:10:48 +0900
committerYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-10 22:10:48 +0900
commite86d83d135e8b8379d03e9ff59690b73c1088e40 (patch)
treeccfdfad42903bb3ca2100b365397b9c2be9b1caf /src/components/SanpiTable.astro
parentcd37eb4e83b3967d74d3fe0d6b06652852a1aa99 (diff)
sanpi/: 議案の賛否一覧を年度別ページに分割
- 概要ページ(/sanpi/)は累計の傾向グラフと年度別リンクのみに - 表と賛否理由は /sanpi/r1d/〜/sanpi/r8d/ の8ページに分割 (3月定例会は前年度に属する) - 傾向グラフは年度別ページの表をビルド時に集計して描画 (src/utils/sanpiStats.ts / src/components/SanpiChart.astro) - SanpiTable.astro は表のツールチップ・全文表示のみに整理
Diffstat (limited to 'src/components/SanpiTable.astro')
-rw-r--r--src/components/SanpiTable.astro161
1 files changed, 5 insertions, 156 deletions
diff --git a/src/components/SanpiTable.astro b/src/components/SanpiTable.astro
index b6b4f26..f17f8f5 100644
--- a/src/components/SanpiTable.astro
+++ b/src/components/SanpiTable.astro
@@ -1,7 +1,9 @@
---
-// 賛否一覧(/sanpi/)の表をスマートフォンでも読みやすくする。
-// 表そのものは Markdown で書かれているため、ここでは目印の要素と、
-// 実行時に表を加工するスクリプトだけを提供する(見た目は custom.css)。
+// 議案の賛否一覧(/sanpi/ と各年度ページ)の表を、スマートフォンでも
+// 読みやすくする。表そのものは Markdown で書かれているため、ここでは
+// 目印の要素と、実行時に表を加工するスクリプトだけを提供する
+// (見た目は custom.css)。会派ごとの傾向グラフは SanpiChart.astro が
+// ビルド時に描画する。
---
<div class="sanpi-marker" hidden aria-hidden="true"></div>
@@ -11,157 +13,6 @@
// タップがホバーとして扱われ、ツールチップが残るのを避けるため)。
const CAN_HOVER = window.matchMedia("(hover: hover) and (pointer: fine)").matches;
- // グラフの先頭に固定する議員(安竹洋平)。
- const ME_NAME = "安竹洋平";
- // 一人会派のグラフ表示名(議員名を添える)。
- const DISPLAY_NAME = { "れ新": "れ新(中倉)", "市民": "市民(水口)" };
-
- function groupLabel(headerText) {
- const text = headerText.replace(/\s+/g, "").trim();
- const m = text.match(/^(.+?)((.+?))$/);
- if (!m) return text;
- const name = m[1];
- const label = m[2];
- // 会派(N人)は会派そのもの。
- if (/^\d+人$/.test(label)) return name;
- // 個人の(政和)は政和会に合算する(外山議員は常に、石津議員も一人でないときは政和会)。
- if (label === "政和") return "政和";
- // (一人)などは議員ごとに分ける。
- return name;
- }
-
- function voteChar(cell) {
- const text = (cell.textContent || "").replace(/\s+/g, "");
- if (/^[〇○◯]$/.test(text)) return "〇";
- if (/^[×✕✖]$/.test(text)) return "×";
- return null;
- }
-
- // 表に個人名が出ない期間は、所属会派の票をその議員に充当する。
- // from/until は定例会(令和X年Y月)を基準にする。until を省略すると現在まで。
- const MEMBER_RULES = [
- // 中江美和議員は令和3年6月からフォーラム小平。
- { name: "中江美和", group: "フォ", from: "令和3年6月" },
- // 石津はるか議員は新議員として政和会に入り、後に一人会派となった。
- { name: "石津はるか", group: "政和", from: "令和3年6月", until: "令和7年3月" },
- ];
-
- function eraKey(text) {
- const m = (text || "").match(/令和(\d+)年(\d+)月/);
- return m ? Number(m[1]) * 100 + Number(m[2]) : null;
- }
-
- // 市が提出した議案か(請願・意見書・決議・条例制定・動議などの議員提出議案を除く)。
- function isCityBill(cell) {
- if (cell.querySelector(".sanpi-shugan")) return false;
- const text = cell.textContent || "";
- return !/意見書|決議|動議|制定/.test(text);
- }
-
- // 各会派が市提出議案にどれだけ賛成したか(賛成と反対の割合)をグラフにする。
- function buildChart(content) {
- const host = content.querySelector(".sanpi-chart");
- if (!host) return;
-
- const stats = new Map();
- const bump = (name, vote) => {
- const s = stats.get(name) || { yes: 0, no: 0 };
- if (vote === "〇") s.yes += 1;
- else s.no += 1;
- stats.set(name, s);
- };
-
- // 会派に合流している期間の票を個人に充当する。
- const applyRules = (groups, cells, sessionKey) => {
- if (sessionKey == null) return;
- MEMBER_RULES.forEach((rule) => {
- const fromKey = eraKey(rule.from);
- const untilKey = rule.until ? eraKey(rule.until) : Infinity;
- if (sessionKey < fromKey || sessionKey > untilKey) return;
- if (groups.includes(rule.name)) return;
- const gi = groups.indexOf(rule.group);
- if (gi < 0) return;
- const vote = voteChar(cells[gi]);
- if (!vote) return;
- bump(rule.name, vote);
- });
- };
-
- // 見出し(令和X年Y月定例会)を追いながら、文書順に表を処理する。
- let sessionKey = null;
- content.querySelectorAll("h2, table").forEach((el) => {
- if (el.tagName === "H2") {
- sessionKey = eraKey(el.textContent);
- return;
- }
- const headers = [...el.querySelectorAll("thead th")].map((th) => th.textContent.trim());
- const groups = headers.map(groupLabel);
- el.querySelectorAll("tbody tr").forEach((tr) => {
- const cells = [...tr.children];
- if (!cells.length || !isCityBill(cells[0])) return;
- cells.forEach((cell, i) => {
- if (i < 2) return;
- const group = groups[i];
- if (!group) return;
- const vote = voteChar(cell);
- if (!vote) return;
- bump(group, vote);
- });
- applyRules(groups, cells, sessionKey);
- });
- });
-
- const rows = [...stats.entries()]
- .map(([group, s]) => {
- const total = s.yes + s.no;
- return { group, yes: s.yes, no: s.no, total, rate: total ? s.yes / total : 0 };
- })
- .filter((row) => row.total > 0)
- // 反対の割合が高い順(=賛成率の低い順)。
- .sort((a, b) => a.rate - b.rate || b.total - a.total || a.group.localeCompare(b.group, "ja"));
-
- if (!rows.length) {
- host.hidden = true;
- return;
- }
-
- const caption = document.createElement("p");
- caption.className = "sanpi-chart-caption";
- caption.textContent =
- "市が提出した議案(請願・意見書・決議・条例制定・動議などの議員提出議案を除く)に対する、会派ごとの議員の賛成・反対の割合です。反対の割合が高い順に並べています。会派が分かれた場合は表の各列の票を1件として数えています。会派に合流している期間(中江美和議員のフォーラム、石津はるか議員の政和会)は、会派の票を個人に充当しています。件数が少ない会派は参考程度にご覧ください。";
-
- const list = document.createElement("ul");
- list.className = "sanpi-chart-list";
- rows.forEach(({ group, yes, no, total, rate }) => {
- const yesPct = Math.round(rate * 100);
- const noPct = 100 - yesPct;
- const item = document.createElement("li");
- item.className = "sanpi-chart-item";
-
- const label = document.createElement("span");
- label.className = "sanpi-chart-label";
- label.textContent = DISPLAY_NAME[group] || group;
- if (group === ME_NAME) label.style.fontWeight = "700";
-
- const bar = document.createElement("span");
- bar.className = "sanpi-chart-bar";
- bar.setAttribute("title", `賛成${yes}件/反対${no}件`);
- const fill = document.createElement("span");
- fill.className = "sanpi-chart-fill";
- fill.style.width = `${yesPct}%`;
- bar.appendChild(fill);
-
- const val = document.createElement("span");
- val.className = "sanpi-chart-val";
- val.innerHTML = `賛成${yesPct}%<small>(${yes})</small>/反対${noPct}%<small>(${no})</small>`;
-
- item.append(label, bar, val);
- list.appendChild(item);
- });
-
- host.append(caption, list);
- }
-
function enhanceSanpiTables() {
const marker = document.querySelector(".sanpi-marker");
if (!marker) return;
@@ -245,8 +96,6 @@
window.addEventListener("scroll", hideTip, { passive: true });
window.addEventListener("resize", hideTip);
-
- buildChart(content);
}
if (document.readyState === "loading") {