Skip to content

Feature/add circle graph and flask#3351

Merged
KATO-Hiro merged 55 commits intostagingfrom
feature/add_circle_graph
Apr 4, 2026
Merged

Feature/add circle graph and flask#3351
KATO-Hiro merged 55 commits intostagingfrom
feature/add_circle_graph

Conversation

@river0525
Copy link
Copy Markdown
Collaborator

@river0525 river0525 commented Apr 3, 2026

投票機能に関する改修です。

以下の変更を行いました。

  • 統計ページの棒グラフをドーナツグラフに変更
  • フラスコアイコンを一覧表・投票ページの暫定グレードに追加

ローカルでcoderabbitによるレビューを4回行いましたが、念のためご確認よろしくお願いいたします。

Summary by CodeRabbit

リリースノート

  • New Features

    • 投票分布を円グラフで可視化
    • 仮評定をアイコンで表示
    • タスク検索機能を追加
  • Refactor

    • 投票ページのUIを再構成(Q/D評定でグループ化)
    • ナビゲーションラベルを「投票」に変更
    • タスク一覧に外部リンクを追加
  • Tests

    • グラフ・検索機能のテストを追加

river0525 and others added 30 commits April 1, 2026 10:02
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ookup

Median grade may have zero votes and therefore no segment, causing the
indicator line to be invisible. Compute the angle from the cumulative
distribution so zero-vote grades are handled correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The chart always starts at the top so the 50th percentile always
falls at the bottom of the ring. Remove getGradeAngle and draw
the line with hardcoded coordinates instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Using userSpaceOnUse with CSS custom property stop-color failed to render.
Switch to objectBoundingBox so the gradient spans the segment itself,
and hardcode the D6 color value (#432414).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- max-w-xs → max-w-md for a larger chart
- Add votedGrade prop; prefix matching segment label with ✅

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- OUTER_RADIUS 90→120, INNER_RADIUS 55→70 (ring width 35→50)
- CX/CY 130→160/155, viewBox 260×275→320×310
- max-w-md→max-w-lg
- Scale up font sizes proportionally

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove ExternalLinkWrapper; clicking the title now navigates to the
contest problem page.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove footnote paragraph and "暫定グレード:" text
- Show flask icon left of grade icon when stats (provisional grade) exist
- Tooltip on flask explains the provisional grade rule

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Navbar: グレード投票 → 投票
- Default: show empty state (search required) to reduce initial load
- Search: limit to 20 results, sorted by task_id desc (newer first)
- Table: outer border + rounded corners, lighter row dividers
- Column order: グレード | 問題名 | 出典 | 票数
- Grade column: flask icon for provisional grades, "-" when no grade
- 問題名: add external link icon to problem page
- 出典: use getContestNameLabel helper
- service: add grade field to TaskWithVoteInfo, sort desc

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
task_id includes the problem letter suffix (_a, _b, ...) and localeCompare
is locale-dependent. contest_id comparison is consistent with
the existing compareByContestIdAndTaskId helper.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add task_table_index to TaskWithVoteInfo so the existing helper can be
used. This matches the grade-based view sort order: contest type
priority → contest_id desc → task_table_index asc.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
KATO-Hiro and others added 12 commits April 4, 2026 01:43
…tion

- Rename arcPath → buildArcPath and extract calcPointOnCircle helper
- Move DonutSegment type to votes/types/donut_graph.ts
- Strengthen grade types from string to TaskGrade domain type
- Replace pct with percentage (full word) and use one decimal place
- Extract VoteDonutChart sections into Svelte 5 snippets
- Update tests: use test() over it(), TaskGrade enums, and rename variables

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add function verb naming rule (get/build/calc/compute prefixes)
- Add snippet parameter type annotation rule (implicit any error)
- Add test stub type rule (domain types, not string)
- Condense Svelte 5 docs fetch note to one line

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/lib/types/task.ts (1)

5-12: 🛠️ Refactor suggestion | 🟠 Major

Task エクスポート型に TSDoc を追加してください。

このファイルは src/**/*types/** 対象で、エクスポート型のTSDoc必須ルールに未準拠です。

修正例
+/**
+ * アプリ内で扱うタスクのドメインモデル。
+ */
 export interface Task {
   contest_type?: ContestType;
   contest_id: string;
   task_table_index: string;
   task_id: string;
   title: string;
   grade: TaskGrade;
 }

As per coding guidelines, src/**/*types/**: Add TSDoc to every exported type.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/types/task.ts` around lines 5 - 12, このファイルでエクスポートされているインターフェース Task に
TSDoc を追加してください: インターフェース宣言直上に短い説明文を含む TSDoc ブロックを追加し、主要フィールド(contest_type,
contest_id, task_table_index, task_id, title, grade)について簡潔な説明行を個別に書いて optional
なフィールドは明記してください(例えば contest_type が任意であること)。TSDoc 内で外部参照(ContestType,
TaskGrade)についても簡単に言及し、既存のインターフェース名 Task をそのまま変更しないでください。
♻️ Duplicate comments (3)
src/features/votes/utils/grade_options.ts (1)

21-30: 🛠️ Refactor suggestion | 🟠 Major

暫定判定もこの utils に寄せてください

resolveDisplayGrade だけだと、grade === TaskGrade.PENDING && displayGrade !== TaskGrade.PENDINGsrc/features/votes/components/VotableGrade.svelte の Line 44-46 と src/routes/votes/+page.svelte の Line 71-72 に残ります。表示グレードと暫定判定を同じ export から返す形にしておくと、条件変更時の片側だけの修正漏れを防げます。

As per coding guidelines, src/**/*.svelte: - Business logic belongs in utils/, not inside <script> blocks.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/votes/utils/grade_options.ts` around lines 21 - 30, The current
resolveDisplayGrade function only returns a TaskGrade, leaving the provisional判定
logic duplicated in VotableGrade.svelte and +page.svelte; change the utils
export to return both the computed display grade and a provisional flag (e.g.
export a function named resolveDisplayGradeWithProvisional or extend
resolveDisplayGrade to return { displayGrade, isProvisional }) so callers
(VotableGrade.svelte's displayGrade check and src/routes/votes/+page.svelte) can
use a single source of truth; update those call sites to consume the new return
shape (use displayGrade where grade was used and use isProvisional for the
previous "grade === TaskGrade.PENDING && displayGrade !== TaskGrade.PENDING"
condition).
src/routes/votes/[slug]/+page.svelte (2)

27-29: 🛠️ Refactor suggestion | 🟠 Major

表示グレード決定ロジックは utils に切り出すべき。

PENDING 時の代替表示ルールは業務ロジックであり、コーディングガイドラインに従い utils/ に移動してください。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routes/votes/`[slug]/+page.svelte around lines 27 - 29, The computed
displayGrade logic in +page.svelte currently embeds business logic (checking
TaskGrade.PENDING and falling back to data.stats.grade) and should be moved into
a pure helper under utils; create a function (e.g. getDisplayGrade(taskGrade,
stats)) in utils that implements "if taskGrade === TaskGrade.PENDING and
stats?.grade return stats.grade else return taskGrade", export it, and replace
the inline derived expression that references
displayGrade/TaskGrade.PENDING/data.task.grade/data.stats?.grade to call that
helper so view code contains only presentation wiring.

42-47: ⚠️ Potential issue | 🟠 Major

Tooltip トリガーがキーボード操作不可。

<span id="flask-icon">tabindex="0"aria-label を追加してください。

修正案
-      <span id="flask-icon" class="cursor-help text-gray-500 dark:text-gray-400">
+      <span
+        id="flask-icon"
+        class="cursor-help text-gray-500 dark:text-gray-400"
+        tabindex="0"
+        aria-label="暫定グレードの説明を表示"
+      >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routes/votes/`[slug]/+page.svelte around lines 42 - 47, The tooltip
trigger span with id "flask-icon" is not keyboard focusable or announced; update
the <span id="flask-icon"> element (the trigger for Tooltip which uses
triggeredBy="#flask-icon") to include tabindex="0" and a meaningful aria-label
(e.g., aria-label="中央値の説明" or similar localized text) so keyboard users can
focus and screen readers can describe the icon; keep the existing id so
Tooltip's triggeredBy reference and the FlaskConical usage remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/dev-notes/2026-04-04/pr-3351-votes/plan.md`:
- Around line 1-63: This markdown file fails Prettier checks; run Prettier to
reformat and commit the change so CI passes: run Prettier (--write) against the
document containing the header "# PR `#3351` レビュー対応メモ" (the affected docs file),
stage the modified file, and push the commit; ensure no substantive content
changes beyond formatting and re-run CI/Prettier --check before updating the PR.

In `@src/features/votes/components/VotableGrade.svelte`:
- Around line 142-156: The button currently uses a fixed aria-label ("Vote
grade") so screen readers can't read the dynamic grade or provisional state;
update the accessible name for the trigger (id uses componentId, onclick calls
onTriggerClick) to include the current GradeLabel value and provisional flag (or
add an sr-only element inside the same button conveying "Grade: <GradeLabel>{,
provisional}" so FlaskConical (used outside the button) is also announced);
ensure the sr-only text is inside the same button element and remove or replace
the static aria-label so the button announces the live grade and provisional
state to assistive tech.

In `@src/features/votes/utils/donut_chart.ts`:
- Line 51: The percentage field uses Math.round(ratio * 1000) / 10 which can
make the total not equal 100; change the logic in donut_chart.ts so you first
compute rawPercent = ratio * 100 for each slice, produce roundedPercent =
Math.round(rawPercent * 10) / 10, then after building the list check the sum of
roundedPercent values and adjust one slice (preferably the largest or the last
slice produced) by delta = 100 - sum to force the total to exactly 100; update
the code that sets percentage (the property currently assigned from
Math.round(ratio * 1000) / 10) to use this adjusted rounded value.

In `@src/routes/votes/`+page.svelte:
- Around line 35-39: The search/filter currently only checks raw contest_id but
the UI displays the contest label from getContestNameLabel(task.contest_id), so
update the filter predicate (the arrow function used in the .filter) to also
compute the display string via getContestNameLabel(t.contest_id) and include
that normalized (toLowerCase) in the includes checks alongside t.title,
t.task_id and t.contest_id so “出典で検索” matches the visible contest name; ensure
you call the existing getContestNameLabel function and handle null/undefined
safely before calling toLowerCase.

---

Outside diff comments:
In `@src/lib/types/task.ts`:
- Around line 5-12: このファイルでエクスポートされているインターフェース Task に TSDoc を追加してください:
インターフェース宣言直上に短い説明文を含む TSDoc ブロックを追加し、主要フィールド(contest_type, contest_id,
task_table_index, task_id, title, grade)について簡潔な説明行を個別に書いて optional
なフィールドは明記してください(例えば contest_type が任意であること)。TSDoc 内で外部参照(ContestType,
TaskGrade)についても簡単に言及し、既存のインターフェース名 Task をそのまま変更しないでください。

---

Duplicate comments:
In `@src/features/votes/utils/grade_options.ts`:
- Around line 21-30: The current resolveDisplayGrade function only returns a
TaskGrade, leaving the provisional判定 logic duplicated in VotableGrade.svelte and
+page.svelte; change the utils export to return both the computed display grade
and a provisional flag (e.g. export a function named
resolveDisplayGradeWithProvisional or extend resolveDisplayGrade to return {
displayGrade, isProvisional }) so callers (VotableGrade.svelte's displayGrade
check and src/routes/votes/+page.svelte) can use a single source of truth;
update those call sites to consume the new return shape (use displayGrade where
grade was used and use isProvisional for the previous "grade ===
TaskGrade.PENDING && displayGrade !== TaskGrade.PENDING" condition).

In `@src/routes/votes/`[slug]/+page.svelte:
- Around line 27-29: The computed displayGrade logic in +page.svelte currently
embeds business logic (checking TaskGrade.PENDING and falling back to
data.stats.grade) and should be moved into a pure helper under utils; create a
function (e.g. getDisplayGrade(taskGrade, stats)) in utils that implements "if
taskGrade === TaskGrade.PENDING and stats?.grade return stats.grade else return
taskGrade", export it, and replace the inline derived expression that references
displayGrade/TaskGrade.PENDING/data.task.grade/data.stats?.grade to call that
helper so view code contains only presentation wiring.
- Around line 42-47: The tooltip trigger span with id "flask-icon" is not
keyboard focusable or announced; update the <span id="flask-icon"> element (the
trigger for Tooltip which uses triggeredBy="#flask-icon") to include
tabindex="0" and a meaningful aria-label (e.g., aria-label="中央値の説明" or similar
localized text) so keyboard users can focus and screen readers can describe the
icon; keep the existing id so Tooltip's triggeredBy reference and the
FlaskConical usage remain unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d8a3134c-cb5f-447a-8fb4-4b6a6d2aa556

📥 Commits

Reviewing files that changed from the base of the PR and between 0cfdb09 and add1ef6.

📒 Files selected for processing (19)
  • .claude/rules/coding-style.md
  • .claude/rules/svelte-components.md
  • .claude/rules/testing.md
  • docs/dev-notes/2026-04-04/pr-3351-votes/plan.md
  • src/features/tasks/fixtures/contest-table/contest_table_provider.ts
  • src/features/votes/components/VotableGrade.svelte
  • src/features/votes/components/VoteDonutChart.svelte
  • src/features/votes/types/donut_graph.ts
  • src/features/votes/utils/donut_chart.test.ts
  • src/features/votes/utils/donut_chart.ts
  • src/features/votes/utils/grade_options.test.ts
  • src/features/votes/utils/grade_options.ts
  • src/features/workbooks/utils/workbooks.ts
  • src/lib/types/task.ts
  • src/routes/votes/+page.svelte
  • src/routes/votes/[slug]/+page.svelte
  • src/routes/workbooks/[slug]/+page.svelte
  • src/test/lib/utils/test_cases/task_results.ts
  • src/test/lib/utils/test_cases/task_table_header_name.ts

GradeLabel handles PENDING internally, so the explicit else branch is unnecessary.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/dev-notes/2026-04-04/pr-3351-votes/plan.md`:
- Around line 59-63: Create a GitHub issue tracking the unresolved TODO about
search-filter mismatch in votes/+page.svelte: describe that the filter currently
matches raw contest_id (t.contest_id) and therefore cannot match the displayed
label returned by getContestNameLabel(), note the misleading placeholder text
"出典", include reproduction steps and UX impact, and list the two proposed
resolutions (A: add getContestNameLabel(t.contest_id).toLowerCase() to the
search filter; B: change the placeholder to match the actual filter behavior)
with A as the recommended user-friendly option; add appropriate labels (bug, ux,
needs-triage), reference votes/+page.svelte and getContestNameLabel(), and
assign or request triage for prioritization.
- Line 50: Update the test-count entry for donut_chart.test.ts in the table row
that mentions `donut_chart.ts` and `buildDonutSegments`/`buildArcPath`: change
the reported "37件" to "13件" so the table accurately reflects that
`donut_chart.test.ts` contains 13 tests covering those functions.
- Line 54: The plan.md justification incorrectly cites a non-existent "plural
alias" rule in AGENTS.md for the `VoteDonutChart.svelte` local alias
`VotedGradeCounters`; update line 54 to either reference the actual naming
conventions that do exist in AGENTS.md (e.g., camelCase / PascalCase /
snake_case / kebab-case) or replace the justification with a correct source
(another doc or style guide) that explains why a plural alias is preferred/kept;
ensure you mention `VoteDonutChart.svelte` and `VotedGradeCounters` in the
justification so reviewers can locate the instance.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b51b004d-ad6e-46f2-9bc4-d96581dcde83

📥 Commits

Reviewing files that changed from the base of the PR and between add1ef6 and a4282b5.

📒 Files selected for processing (1)
  • docs/dev-notes/2026-04-04/pr-3351-votes/plan.md

- Replace generic aria-label with sr-only span carrying grade and provisional state
- Include contest name label in search filter so users can search by display name

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@KATO-Hiro

This comment was marked as outdated.

…e support

- Extract search filter logic into src/lib/utils/task_filter.ts
- Add contest name label matching so users can search by display name (e.g. "ABC 300")
- Add unit tests for filterTasksBySearch
- Remove completed plan.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/routes/votes/`+page.svelte:
- Around line 40-41: The search input lacks an accessible name because it only
uses a placeholder; update the Input component usage (the JSX/markup line with
Input bind:value={search}) to provide an explicit accessible name by either
adding an aria-label (e.g., aria-label="検索" or "Search problems") on the Input
component or by adding a corresponding <label for="..."> and giving the Input an
id, ensuring the label text describes the field; make sure the chosen
label/aria-label is concise and localized if needed and that it matches the
Input instance bound to the search variable.
- Around line 32-34: filteredTasks currently applies
sort(compareByContestIdAndTaskId) after filterTasksBySearch, but
filterTasksBySearch already slices to limit so sorting after causes wrong
results when >20 matches; fix by sorting the full task list before filtering:
pass a sorted copy of data.tasks (e.g.
[...data.tasks].sort(compareByContestIdAndTaskId)) into filterTasksBySearch
instead of sorting the filtered/sliced result so filteredTasks,
filterTasksBySearch, compareByContestIdAndTaskId and MAX_SEARCH_RESULTS produce
the correct display order.

In `@src/test/lib/utils/task_filter.test.ts`:
- Around line 35-40: The test uses filterTasksBySearch to match by contest-name
label but the search term 'ABC 300' also appears in the task title, so the
branch that checks getContestNameLabel isn't exercised; update the fixture and
test to use a contest id whose human label appears only via getContestNameLabel
(e.g. replace 'abc300'/'ABC 300' with 'typical90' and its derived label like
'競プロ典型 90 問'), ensure the task's title does NOT contain that label, call
filterTasksBySearch with the derived label, and assert the expected task_id so
the getContestNameLabel path in filterTasksBySearch is actually covered.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 660f0207-93a2-4d51-b065-a2c7518fe8aa

📥 Commits

Reviewing files that changed from the base of the PR and between a4282b5 and 109782c.

📒 Files selected for processing (4)
  • src/features/votes/components/VotableGrade.svelte
  • src/lib/utils/task_filter.ts
  • src/routes/votes/+page.svelte
  • src/test/lib/utils/task_filter.test.ts

Copy link
Copy Markdown
Collaborator

@KATO-Hiro KATO-Hiro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@river0525

UIの大幅な修正をしていただき、ありがとうございます
とても見やすくなっていると思います

LGTMです

マージします

KATO-Hiro and others added 2 commits April 4, 2026 08:03
Previously sort() was applied after filter(), which mutated a derived array
on every keystroke. Pre-sort via sortedTasks derived state ensures stable
order regardless of search input.

Also strengthen test: use typical90 fixture to isolate contest name label
matching, and add order-preservation test.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@KATO-Hiro KATO-Hiro merged commit 3251a20 into staging Apr 4, 2026
3 checks passed
@KATO-Hiro KATO-Hiro deleted the feature/add_circle_graph branch April 4, 2026 08:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants