Skip to content

Commit a462cce

Browse files
committed
feat(charts): add MemberContributionPie component
1 parent 396defb commit a462cce

7 files changed

Lines changed: 157 additions & 45 deletions

File tree

src/Directory.Packages.props

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,8 @@
1616
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Protocols.Json" Version="9.0.7" />
1717
<PackageVersion Include="Microsoft.AspNetCore.SpaProxy" Version="9.0.7" />
1818
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="9.0.7" />
19-
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tasks" Version="9.0.7">
20-
<PrivateAssets>all</PrivateAssets>
21-
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
22-
</PackageVersion>
23-
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.7">
24-
<PrivateAssets>all</PrivateAssets>
25-
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
26-
</PackageVersion>
19+
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tasks" Version="9.0.7" />
20+
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.7" />
2721
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.Common" Version="9.7.0" />
2822
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
2923
<PackageVersion Include="Npgsql.OpenTelemetry" Version="9.0.3" />
@@ -71,4 +65,4 @@
7165
<PackageVersion Include="coverlet.msbuild" Version="6.0.4" />
7266
<PackageVersion Include="ZstdSharp.Port" Version="0.8.6" />
7367
</ItemGroup>
74-
</Project>
68+
</Project>

src/GZCTF/ClientApp/src/components/ChallengeModal.tsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
} from '@mantine/core'
1414
import { mdiLightbulbOnOutline, mdiOpenInNew, mdiPackageVariantClosed } from '@mdi/js'
1515
import Icon from '@mdi/react'
16-
import { FC, useCallback, useEffect, useState } from 'react'
16+
import { FC, useEffect, useState } from 'react'
1717
import { useTranslation } from 'react-i18next'
1818
import { InstanceEntry } from '@Components/InstanceEntry'
1919
import { ContentPlaceholder, InlineMarkdown, Markdown } from '@Components/MarkdownRenderer'
@@ -182,11 +182,6 @@ export const ChallengeModal: FC<ChallengeModalProps> = (props) => {
182182
</Stack>
183183
)
184184

185-
const cachedScrollAreaComponent = useCallback<React.FC<any>>(
186-
({ style, ...props }) => <div {...props} style={{ ...style }} />,
187-
[]
188-
)
189-
190185
return (
191186
<Modal.Root
192187
size="40vw"
@@ -196,7 +191,6 @@ export const ChallengeModal: FC<ChallengeModalProps> = (props) => {
196191
modalProps.onClose()
197192
}}
198193
centered
199-
scrollAreaComponent={cachedScrollAreaComponent}
200194
classNames={classes}
201195
>
202196
<Modal.Overlay />

src/GZCTF/ClientApp/src/components/ScoreboardItemModal.tsx

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
Badge,
44
Center,
55
Group,
6-
LoadingOverlay,
76
Modal,
87
ModalProps,
98
Progress,
@@ -17,7 +16,9 @@ import {
1716
import dayjs from 'dayjs'
1817
import { FC } from 'react'
1918
import { useTranslation } from 'react-i18next'
20-
import { TeamRadarMap } from '@Components/TeamRadarMap'
19+
import { MemberContributionPie } from '@Components/charts/MemberContributionPie'
20+
import { MemberContributionPieProps } from '@Components/charts/MemberContributionPie'
21+
import { TeamRadarMap, TeamRadarMapProps } from '@Components/charts/TeamRadarMap'
2122
import { useLanguage } from '@Utils/I18n'
2223
import { BloodsTypes, BonusLabel } from '@Utils/Shared'
2324
import { ChallengeInfo, ScoreboardItem, ScoreboardModel, SubmissionType } from '@Api'
@@ -30,12 +31,52 @@ export interface ScoreboardItemModalProps extends ModalProps {
3031
scoreboard?: ScoreboardModel
3132
}
3233

34+
function calculateScoreRadar(
35+
challenges: Record<string, ChallengeInfo[]>,
36+
challengeIdMap: Map<number, ChallengeInfo>,
37+
item?: ScoreboardItem
38+
): TeamRadarMapProps {
39+
const indicator =
40+
challenges &&
41+
Object.keys(challenges).map((cate) => ({
42+
name: cate,
43+
scoreSum: challenges[cate].reduce((sum, chal) => sum + (!chal.solved ? 0 : chal.score!), 0),
44+
max: 1,
45+
}))
46+
47+
const value = indicator?.map((ind) => {
48+
const solvedChallenges = item?.solvedChallenges?.filter(
49+
(chal) => challengeIdMap?.get(chal.id!)?.category === ind.name
50+
)
51+
const cateScore = solvedChallenges?.reduce((sum, chal) => sum + chal.score!, 0) ?? 0
52+
return Math.min(cateScore / ind.scoreSum, 1)
53+
})
54+
55+
return { indicator, value, name: item?.name ?? '' }
56+
}
57+
58+
function calculateMemberContribution(item?: ScoreboardItem): MemberContributionPieProps {
59+
const memberScores =
60+
item?.solvedChallenges?.reduce((acc, chal) => {
61+
const score = acc.get(chal.userName!) ?? 0
62+
acc.set(chal.userName!, score + chal.score!)
63+
return acc
64+
}, new Map<string, number>()) ?? new Map<string, number>()
65+
66+
const data = Array.from(memberScores.entries()).map(([name, value]) => ({
67+
name,
68+
value,
69+
}))
70+
71+
data.sort((a, b) => b.value - a.value)
72+
73+
return { data }
74+
}
75+
3376
export const ScoreboardItemModal: FC<ScoreboardItemModalProps> = (props) => {
3477
const { item, scoreboard, bloodBonusMap, ...modalProps } = props
35-
3678
const { t } = useTranslation()
3779
const { locale } = useLanguage()
38-
3980
const challenges = scoreboard?.challenges
4081
const challengeIdMap =
4182
challenges &&
@@ -46,24 +87,9 @@ export const ScoreboardItemModal: FC<ScoreboardItemModalProps> = (props) => {
4687
return map
4788
}, new Map<number, ChallengeInfo>())
4889

90+
const valid = item && challenges && challengeIdMap
4991
const solved = (item?.solvedCount ?? 0) / (scoreboard?.challengeCount ?? 1)
5092

51-
const indicator =
52-
challenges &&
53-
Object.keys(challenges).map((cate) => ({
54-
name: cate,
55-
scoreSum: challenges[cate].reduce((sum, chal) => sum + (!chal.solved ? 0 : chal.score!), 0),
56-
max: 1,
57-
}))
58-
59-
const values = indicator?.map((ind) => {
60-
const solvedChallenges = item?.solvedChallenges?.filter(
61-
(chal) => challengeIdMap?.get(chal.id!)?.category === ind.name
62-
)
63-
const cateScore = solvedChallenges?.reduce((sum, chal) => sum + chal.score!, 0) ?? 0
64-
return Math.min(cateScore / ind.scoreSum, 1)
65-
})
66-
6793
return (
6894
<Modal
6995
{...modalProps}
@@ -91,11 +117,13 @@ export const ScoreboardItemModal: FC<ScoreboardItemModalProps> = (props) => {
91117
}
92118
>
93119
<Stack align="center" gap="xs">
94-
<Stack w="60%" miw="20rem">
120+
<Stack w="85%" miw="20rem">
95121
<Center h="14rem">
96-
<LoadingOverlay visible={!indicator || !values} />
97-
{item && indicator && values && (
98-
<TeamRadarMap indicator={indicator} value={values} name={item?.name ?? ''} />
122+
{valid && (
123+
<Group wrap="nowrap" gap={0} justify="center" w="100%" h="100%">
124+
<TeamRadarMap {...calculateScoreRadar(challenges, challengeIdMap, item)} />
125+
<MemberContributionPie {...calculateMemberContribution(item)} />
126+
</Group>
99127
)}
100128
</Center>
101129
<Group grow ta="center">
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { useMantineColorScheme, useMantineTheme } from '@mantine/core'
2+
import chroma from 'chroma-js'
3+
import ReactEcharts from 'echarts-for-react'
4+
import { FC, useMemo } from 'react'
5+
6+
export interface MemberContributionPieProps {
7+
data: { name: string; value: number }[]
8+
}
9+
10+
export const MemberContributionPie: FC<MemberContributionPieProps> = ({ data }) => {
11+
const theme = useMantineTheme()
12+
const { colorScheme } = useMantineColorScheme()
13+
14+
const labelColor = colorScheme === 'dark' ? theme.colors.light[1] : theme.colors.dark[5]
15+
const backgroundColor = colorScheme === 'dark' ? theme.colors.gray[6] : theme.colors.light[1]
16+
17+
const colorPalette = useMemo(() => {
18+
const [start, end] = colorScheme === 'dark' ? [3, 7] : [4, 8]
19+
return chroma
20+
.scale([theme.colors[theme.primaryColor][start], theme.colors[theme.primaryColor][end]])
21+
.mode('oklch')
22+
.colors(data.length)
23+
}, [data.length, theme.primaryColor, theme.colors, colorScheme])
24+
25+
return (
26+
<ReactEcharts
27+
theme={colorScheme}
28+
option={{
29+
animation: true,
30+
backgroundColor: 'transparent',
31+
color: colorPalette,
32+
tooltip: {
33+
trigger: 'item',
34+
formatter: '{b}: {c} ({d}%)',
35+
borderWidth: 0,
36+
textStyle: {
37+
fontSize: 12,
38+
color: labelColor,
39+
},
40+
backgroundColor: backgroundColor,
41+
},
42+
legend: {
43+
orient: 'horizontal',
44+
left: 'center',
45+
top: 'bottom',
46+
data: data.map((item) => item.name),
47+
textStyle: {
48+
color: labelColor,
49+
fontWeight: 'bold',
50+
},
51+
},
52+
series: [
53+
{
54+
type: 'pie',
55+
radius: ['45%', '65%'],
56+
center: ['50%', '45%'],
57+
avoidLabelOverlap: false,
58+
itemStyle: {
59+
borderRadius: 8,
60+
borderWidth: 0,
61+
},
62+
label: {
63+
show: false,
64+
position: 'center',
65+
},
66+
emphasis: {
67+
label: {
68+
show: true,
69+
fontSize: 14,
70+
fontWeight: 'bold',
71+
},
72+
},
73+
labelLine: {
74+
show: false,
75+
},
76+
data,
77+
},
78+
],
79+
}}
80+
opts={{
81+
renderer: 'svg',
82+
}}
83+
style={{
84+
width: '100%',
85+
height: '100%',
86+
display: 'flex',
87+
}}
88+
/>
89+
)
90+
}

src/GZCTF/ClientApp/src/components/charts/ScoreTimeLine.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ export const ScoreTimeLine: FC<TimeLineProps> = ({ division }) => {
133133
trigger: 'axis',
134134
borderWidth: 0,
135135
textStyle: {
136-
fontSize: 10,
136+
fontSize: 12,
137137
color: labelColor,
138138
},
139139
backgroundColor: backgroundColor,

src/GZCTF/ClientApp/src/components/TeamRadarMap.tsx renamed to src/GZCTF/ClientApp/src/components/charts/TeamRadarMap.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { alpha, useMantineColorScheme, useMantineTheme } from '@mantine/core'
22
import ReactEcharts from 'echarts-for-react'
33
import { FC } from 'react'
44

5-
interface TeamRadarMapProps {
5+
export interface TeamRadarMapProps {
66
indicator: { name: string; max: number }[]
77
value: number[]
88
name: string
@@ -16,13 +16,15 @@ export const TeamRadarMap: FC<TeamRadarMapProps> = ({ indicator, value, name })
1616
<ReactEcharts
1717
theme={colorScheme}
1818
option={{
19-
animation: false,
19+
animation: true,
2020
color: theme.colors[theme.primaryColor][5],
2121
backgroundColor: 'transparent',
2222
radar: {
2323
indicator,
24+
shape: 'circle',
2425
axisName: {
2526
color: colorScheme === 'dark' ? theme.colors.light[1] : theme.colors.dark[5],
27+
fontWeight: 'bold',
2628
},
2729
},
2830
series: [
@@ -33,8 +35,12 @@ export const TeamRadarMap: FC<TeamRadarMapProps> = ({ indicator, value, name })
3335
value,
3436
name,
3537
areaStyle: {
36-
color: alpha(theme.colors[theme.primaryColor][4], 0.6),
38+
color: alpha(theme.colors[theme.primaryColor][4], 0.8),
3739
},
40+
lineStyle: {
41+
width: 2,
42+
},
43+
symbolSize: 4,
3844
},
3945
],
4046
},

src/GZCTF/ClientApp/src/components/mobile/ScoreboardItemModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import dayjs from 'dayjs'
1717
import { FC } from 'react'
1818
import { useTranslation } from 'react-i18next'
1919
import { ScoreboardItemModalProps } from '@Components/ScoreboardItemModal'
20-
import { TeamRadarMap } from '@Components/TeamRadarMap'
20+
import { TeamRadarMap } from '@Components/charts/TeamRadarMap'
2121
import { useLanguage } from '@Utils/I18n'
2222
import { ChallengeInfo } from '@Api'
2323
import inputClasses from '@Styles/Input.module.css'

0 commit comments

Comments
 (0)