From e86d83d135e8b8379d03e9ff59690b73c1088e40 Mon Sep 17 00:00:00 2001 From: Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> Date: Sat, 10 Oct 2026 22:10:48 +0900 Subject: sanpi/: 議案の賛否一覧を年度別ページに分割 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 概要ページ(/sanpi/)は累計の傾向グラフと年度別リンクのみに - 表と賛否理由は /sanpi/r1d/〜/sanpi/r8d/ の8ページに分割 (3月定例会は前年度に属する) - 傾向グラフは年度別ページの表をビルド時に集計して描画 (src/utils/sanpiStats.ts / src/components/SanpiChart.astro) - SanpiTable.astro は表のツールチップ・全文表示のみに整理 --- src/components/SanpiTable.astro | 161 ++-------------------------------------- 1 file changed, 5 insertions(+), 156 deletions(-) (limited to 'src/components/SanpiTable.astro') 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 が +// ビルド時に描画する。 --- @@ -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}%(${yes})/反対${noPct}%(${no})`; - - 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") { -- cgit v1.3.1