-
-
Notifications
You must be signed in to change notification settings - Fork 10
Feature/add circle graph and flask #3351
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
55 commits
Select commit
Hold shift + click to select a range
7615f0e
feat: add SVG donut chart utility functions
river0525 cdc8cce
feat: add VoteDonutChart SVG donut chart component
river0525 28ff243
feat: replace bar chart with donut chart on vote detail page
river0525 aa52a72
feat: add median grade indicator line to donut chart
river0525 d896cdd
fix: draw median line using cumulative arc angle instead of segment l…
river0525 a3cd341
refactor: simplify median line to fixed bottom position
river0525 1f0e1db
feat: change median line to black and add label
river0525 f170641
feat: move grade labels inside donut ring segments
river0525 c945a06
feat: apply metallic gradient to D6 segment in donut chart
river0525 f63f3ef
fix: use objectBoundingBox gradient for D6 metallic effect
river0525 c379548
fix: remove arrow prefix from median label
river0525 1d90d0e
feat: enlarge donut chart and show checkmark on voted grade segment
river0525 25bf03c
feat: move vote change form above donut chart
river0525 2e989af
feat: enlarge donut chart size and ring thickness
river0525 0f50ab9
feat: increase donut ring width (INNER_RADIUS 70→50)
river0525 cb962e2
feat: place grade icon left of title and link title to contest page
river0525 2dd52b0
feat: remove voted status text (replaced by checkmark in donut chart)
river0525 50d6472
feat: open contest page in new tab when clicking title
river0525 02185c6
feat: display vote buttons in two rows (Q-tier and D-tier)
river0525 0310dc6
feat: unify vote button width with w-12
river0525 6072b30
feat: always show vote form without details toggle
river0525 5e0f8c0
feat: replace provisional grade text with flask icon tooltip
river0525 0c9382c
feat: hide flask icon when grade is confirmed (non-PENDING)
river0525 02c46ed
feat: replace title attribute with Flowbite Tooltip on flask icon
river0525 51a1806
feat: show provisional grade in grade icon when task grade is PENDING
river0525 0fde56f
fix: show provisional grade only when task grade is PENDING
river0525 409b10d
feat: overhaul /votes list page per review feedback
river0525 8d7ec40
fix: explicitly sort search results by task_id descending on client side
river0525 ab92440
fix: sort search results by contest_id desc instead of task_id
river0525 52d507c
fix: sort votes list using compareByContestIdAndTaskId helper
river0525 67b38ff
feat: show flask icon next to provisional grade in contest table
river0525 0cfdb09
fix: address CodeRabbit findings (round 1)
river0525 7b6ab11
Merge branch 'staging' of github.com:AtCoder-NoviSteps/AtCoderNoviSte…
KATO-Hiro c22586f
fix(votes): handle full-circle arc segment via dual-arc split
KATO-Hiro e7a0839
fix(votes): use ring midpoint radius and update arcPath center arg
KATO-Hiro 1a0e5b8
refactor(votes): extract resolveDisplayGrade to grade_options utility
KATO-Hiro 92f32eb
fix(votes): make flask icon tooltip keyboard-accessible via Flowbite …
KATO-Hiro 72a18a4
fix(votes): add noopener to external link rel attributes
KATO-Hiro dc204ef
fix(votes): unify nav label from 'グレード投票' to '投票' in breadcrumb
KATO-Hiro 7a141be
style(votes): increase table header and body font size
KATO-Hiro 5594ce0
style(votes): use default GradeLabel size to match workbooks page
KATO-Hiro 06f67fa
style(votes): move flask icon to the left of grade label in VotableGrade
KATO-Hiro 5c10032
docs(votes): add TSDoc to grade_options exports
KATO-Hiro 8f465e0
refactor: strengthen Task.grade from string to TaskGrade domain type
KATO-Hiro 3998e90
docs: Add rule
KATO-Hiro 5e1278d
docs: update plan
KATO-Hiro 5dec1f4
refactor(votes): apply code review findings to donut chart implementa…
KATO-Hiro 98793da
docs(rules): capture lessons from votes donut chart refactor
KATO-Hiro add1ef6
docs(votes): convert plan.md to completion memo and remove review.md
KATO-Hiro a4282b5
chore: Fix format
KATO-Hiro 471fbf9
refactor(votes): remove redundant PENDING branch in grade display
KATO-Hiro 9ad09ac
fix(votes): improve accessibility and search usability
KATO-Hiro 109782c
refactor(votes): extract filterTasksBySearch utility with contest nam…
KATO-Hiro cb793eb
fix(votes): sort tasks before filtering to preserve stable order
KATO-Hiro 6cd434d
chore: fix typo
KATO-Hiro File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,194 @@ | ||
| <script lang="ts"> | ||
| import type { VotedGradeCounter } from '@prisma/client'; | ||
| import { TaskGrade } from '$lib/types/task'; | ||
| import type { DonutSegment } from '$features/votes/types/donut_graph'; | ||
|
|
||
| import { getTaskGradeColor, getTaskGradeLabel } from '$lib/utils/task'; | ||
| import { nonPendingGrades } from '$features/votes/utils/grade_options'; | ||
| import { | ||
| buildDonutSegments, | ||
| buildArcPath, | ||
| calcPointOnCircle, | ||
| } from '$features/votes/utils/donut_chart'; | ||
|
|
||
| type VotedGradeCounters = VotedGradeCounter[]; | ||
|
|
||
| interface Props { | ||
| counters: VotedGradeCounters; | ||
| totalVotes: number; | ||
| /** Median grade to indicate with a radial line. Omit when stats are unavailable. */ | ||
| medianGrade?: TaskGrade | null; | ||
| /** The grade the current user voted for. Shows a ✅ on the matching segment. */ | ||
| votedGrade?: TaskGrade | null; | ||
| } | ||
| let { counters, totalVotes, medianGrade = null, votedGrade = null }: Props = $props(); | ||
|
|
||
| const CX = 160; | ||
| const CY = 155; | ||
| const OUTER_RADIUS = 120; | ||
| const INNER_RADIUS = 50; | ||
| const RING_MID_RADIUS = (INNER_RADIUS + OUTER_RADIUS) / 2; | ||
|
|
||
| const segments = $derived( | ||
| buildDonutSegments(nonPendingGrades, counters, getTaskGradeColor, getTaskGradeLabel), | ||
| ); | ||
| </script> | ||
|
|
||
| <svg viewBox="0 0 320 310" class="w-full max-w-lg mx-auto" role="img" aria-label="投票分布円グラフ"> | ||
| <title>投票分布</title> | ||
| <defs> | ||
| <!-- Metallic gradient for D6 segment, matching the vote button style. | ||
| objectBoundingBox ensures the gradient spans the segment itself. --> | ||
| {@render metallicGradient()} | ||
| </defs> | ||
|
|
||
| {#if totalVotes >= 1} | ||
| {#each segments as segment (segment.grade)} | ||
| {@render segmentPath(segment)} | ||
| {/each} | ||
|
|
||
| <!-- Median grade indicator line: the chart starts at the top and the median | ||
| always falls at the 50% cumulative mark, which is fixed at the bottom. --> | ||
| {#if medianGrade} | ||
| {@render medianIndicator()} | ||
| {/if} | ||
|
|
||
| {#each segments as segment (segment.grade)} | ||
| {@render segmentLabel(segment)} | ||
| {/each} | ||
| {:else} | ||
| <!-- Show empty ring when there are no votes --> | ||
| {@render emptyRing()} | ||
| {/if} | ||
|
|
||
| {@render totalVotedCount()} | ||
| </svg> | ||
|
|
||
| {#snippet metallicGradient()} | ||
| <linearGradient id="d6-metallic" x1="0%" y1="0%" x2="100%" y2="100%"> | ||
| <stop offset="0%" stop-color="#432414" /> | ||
| <stop offset="40%" stop-color="rgb(120, 113, 108)" /> | ||
| <stop offset="70%" stop-color="rgb(217, 119, 6)" /> | ||
| <stop offset="100%" stop-color="#432414" /> | ||
| </linearGradient> | ||
| {/snippet} | ||
|
|
||
| {#snippet segmentPath(segment: DonutSegment)} | ||
| <path | ||
| d={buildArcPath( | ||
| { x: CX, y: CY }, | ||
| OUTER_RADIUS, | ||
| INNER_RADIUS, | ||
| segment.startAngle, | ||
| segment.endAngle, | ||
| )} | ||
| fill={segment.grade === TaskGrade.D6 ? 'url(#d6-metallic)' : segment.color} | ||
| /> | ||
| {/snippet} | ||
|
|
||
| {#snippet medianIndicator()} | ||
| <line | ||
| x1={CX} | ||
| y1={CY + INNER_RADIUS} | ||
| x2={CX} | ||
| y2={CY + OUTER_RADIUS} | ||
| stroke="black" | ||
| stroke-width="2.5" | ||
| stroke-linecap="round" | ||
| class="dark:stroke-white" | ||
| /> | ||
| <text | ||
| x={CX} | ||
| y={CY + OUTER_RADIUS + 14} | ||
| text-anchor="middle" | ||
| class="fill-gray-700 dark:fill-gray-300" | ||
| font-size="11">中央値</text | ||
| > | ||
| {/snippet} | ||
|
|
||
| {#snippet segmentLabel(segment: DonutSegment)} | ||
| {@const labelPoint = calcPointOnCircle({ x: CX, y: CY }, RING_MID_RADIUS, segment.midAngle)} | ||
| {@const labelX = labelPoint.x} | ||
| {@const labelY = labelPoint.y} | ||
| {@const label = segment.grade === votedGrade ? `✅ ${segment.label}` : segment.label} | ||
|
|
||
| {#if segment.percentage >= 10} | ||
| <text | ||
| x={labelX} | ||
| y={labelY - 7} | ||
| text-anchor="middle" | ||
| dominant-baseline="middle" | ||
| fill="white" | ||
| stroke="rgba(0,0,0,0.55)" | ||
| stroke-width="2.5" | ||
| paint-order="stroke" | ||
| font-size="11" | ||
| font-weight="bold" | ||
| > | ||
| {label} | ||
| </text> | ||
| <text | ||
| x={labelX} | ||
| y={labelY + 8} | ||
| text-anchor="middle" | ||
| dominant-baseline="middle" | ||
| fill="white" | ||
| stroke="rgba(0,0,0,0.55)" | ||
| stroke-width="2" | ||
| paint-order="stroke" | ||
| font-size="9.5" | ||
| > | ||
| {segment.count}票 ({segment.percentage}%) | ||
| </text> | ||
| {:else if segment.percentage >= 5} | ||
| <text | ||
| x={labelX} | ||
| y={labelY} | ||
| text-anchor="middle" | ||
| dominant-baseline="middle" | ||
| fill="white" | ||
| stroke="rgba(0,0,0,0.55)" | ||
| stroke-width="2.5" | ||
| paint-order="stroke" | ||
| font-size="11" | ||
| font-weight="bold" | ||
| > | ||
| {label} | ||
| </text> | ||
| {/if} | ||
| {/snippet} | ||
|
|
||
| {#snippet emptyRing()} | ||
| <circle | ||
| cx={CX} | ||
| cy={CY} | ||
| r={RING_MID_RADIUS} | ||
| fill="none" | ||
| stroke="currentColor" | ||
| stroke-width={OUTER_RADIUS - INNER_RADIUS} | ||
| class="text-gray-200 dark:text-gray-700" | ||
| opacity="0.5" | ||
| /> | ||
| {/snippet} | ||
|
|
||
| {#snippet totalVotedCount()} | ||
| <text | ||
| x={CX} | ||
| y={CY - 6} | ||
| text-anchor="middle" | ||
| class="fill-gray-800 dark:fill-gray-200" | ||
| font-size="28" | ||
| font-weight="bold" | ||
| > | ||
| {totalVotes} | ||
| </text> | ||
| <text | ||
| x={CX} | ||
| y={CY + 16} | ||
| text-anchor="middle" | ||
| class="fill-gray-500 dark:fill-gray-400" | ||
| font-size="13" | ||
| > | ||
| 票 | ||
| </text> | ||
| {/snippet} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.