add echarts chart components for gw scores
This commit is contained in:
parent
807cb8fb96
commit
0712fb20c0
7 changed files with 481 additions and 0 deletions
|
|
@ -39,6 +39,7 @@
|
||||||
"@tanstack/svelte-query-devtools": "^6.0.2",
|
"@tanstack/svelte-query-devtools": "^6.0.2",
|
||||||
"@types/node": "^22",
|
"@types/node": "^22",
|
||||||
"@vitest/browser": "^3.2.3",
|
"@vitest/browser": "^3.2.3",
|
||||||
|
"echarts": "^5.6.0",
|
||||||
"eslint": "^9.18.0",
|
"eslint": "^9.18.0",
|
||||||
"eslint-config-prettier": "^10.0.1",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
"eslint-plugin-storybook": "^10.1.2",
|
"eslint-plugin-storybook": "^10.1.2",
|
||||||
|
|
@ -52,6 +53,7 @@
|
||||||
"storybook": "^10.1.2",
|
"storybook": "^10.1.2",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.0.0",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.0.0",
|
||||||
|
"svelte-echarts": "^1.0.0",
|
||||||
"svelte-preprocess": "^6.0.3",
|
"svelte-preprocess": "^6.0.3",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
"typescript-eslint": "^8.20.0",
|
"typescript-eslint": "^8.20.0",
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,9 @@ importers:
|
||||||
'@vitest/browser':
|
'@vitest/browser':
|
||||||
specifier: ^3.2.3
|
specifier: ^3.2.3
|
||||||
version: 3.2.4(playwright@1.55.0)(vite@7.1.5(@types/node@22.18.1)(sass@1.92.1))(vitest@3.2.4)
|
version: 3.2.4(playwright@1.55.0)(vite@7.1.5(@types/node@22.18.1)(sass@1.92.1))(vitest@3.2.4)
|
||||||
|
echarts:
|
||||||
|
specifier: ^5.6.0
|
||||||
|
version: 5.6.0
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^9.18.0
|
specifier: ^9.18.0
|
||||||
version: 9.35.0
|
version: 9.35.0
|
||||||
|
|
@ -147,6 +150,9 @@ importers:
|
||||||
svelte-check:
|
svelte-check:
|
||||||
specifier: ^4.0.0
|
specifier: ^4.0.0
|
||||||
version: 4.3.1(picomatch@4.0.3)(svelte@5.38.7)(typescript@5.9.2)
|
version: 4.3.1(picomatch@4.0.3)(svelte@5.38.7)(typescript@5.9.2)
|
||||||
|
svelte-echarts:
|
||||||
|
specifier: ^1.0.0
|
||||||
|
version: 1.0.0(echarts@5.6.0)(svelte@5.38.7)
|
||||||
svelte-preprocess:
|
svelte-preprocess:
|
||||||
specifier: ^6.0.3
|
specifier: ^6.0.3
|
||||||
version: 6.0.3(postcss-load-config@3.1.4(postcss@8.5.6))(postcss@8.5.6)(sass@1.92.1)(svelte@5.38.7)(typescript@5.9.2)
|
version: 6.0.3(postcss-load-config@3.1.4(postcss@8.5.6))(postcss@8.5.6)(sass@1.92.1)(svelte@5.38.7)(typescript@5.9.2)
|
||||||
|
|
@ -1607,6 +1613,9 @@ packages:
|
||||||
dom-accessibility-api@0.6.3:
|
dom-accessibility-api@0.6.3:
|
||||||
resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==}
|
resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==}
|
||||||
|
|
||||||
|
echarts@5.6.0:
|
||||||
|
resolution: {integrity: sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==}
|
||||||
|
|
||||||
entities@4.5.0:
|
entities@4.5.0:
|
||||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||||
engines: {node: '>=0.12'}
|
engines: {node: '>=0.12'}
|
||||||
|
|
@ -2519,6 +2528,12 @@ packages:
|
||||||
svelte: ^4.0.0 || ^5.0.0-next.0
|
svelte: ^4.0.0 || ^5.0.0-next.0
|
||||||
typescript: '>=5.0.0'
|
typescript: '>=5.0.0'
|
||||||
|
|
||||||
|
svelte-echarts@1.0.0:
|
||||||
|
resolution: {integrity: sha512-o0VgdxGJt+Km+IrxNo35qTJFqDsvj65p7JwAlIrk7CsWjBKKniJaUV6hKbMDNf0S34Su0idWbWEdymJNPX3anA==}
|
||||||
|
peerDependencies:
|
||||||
|
echarts: ^5.0.0
|
||||||
|
svelte: '>=5'
|
||||||
|
|
||||||
svelte-eslint-parser@1.3.1:
|
svelte-eslint-parser@1.3.1:
|
||||||
resolution: {integrity: sha512-0Iztj5vcOVOVkhy1pbo5uA9r+d3yaVoE5XPc9eABIWDOSJZ2mOsZ4D+t45rphWCOr0uMw3jtSG2fh2e7GvKnPg==}
|
resolution: {integrity: sha512-0Iztj5vcOVOVkhy1pbo5uA9r+d3yaVoE5XPc9eABIWDOSJZ2mOsZ4D+t45rphWCOr0uMw3jtSG2fh2e7GvKnPg==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
@ -2638,6 +2653,9 @@ packages:
|
||||||
resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==}
|
resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==}
|
||||||
engines: {node: '>=6.10'}
|
engines: {node: '>=6.10'}
|
||||||
|
|
||||||
|
tslib@2.3.0:
|
||||||
|
resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
|
||||||
|
|
||||||
tslib@2.8.1:
|
tslib@2.8.1:
|
||||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||||
|
|
||||||
|
|
@ -2885,6 +2903,9 @@ packages:
|
||||||
zod@4.1.5:
|
zod@4.1.5:
|
||||||
resolution: {integrity: sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg==}
|
resolution: {integrity: sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg==}
|
||||||
|
|
||||||
|
zrender@5.6.1:
|
||||||
|
resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==}
|
||||||
|
|
||||||
zwitch@2.0.4:
|
zwitch@2.0.4:
|
||||||
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
|
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
|
||||||
|
|
||||||
|
|
@ -4291,6 +4312,11 @@ snapshots:
|
||||||
|
|
||||||
dom-accessibility-api@0.6.3: {}
|
dom-accessibility-api@0.6.3: {}
|
||||||
|
|
||||||
|
echarts@5.6.0:
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.3.0
|
||||||
|
zrender: 5.6.1
|
||||||
|
|
||||||
entities@4.5.0: {}
|
entities@4.5.0: {}
|
||||||
|
|
||||||
es-module-lexer@1.7.0: {}
|
es-module-lexer@1.7.0: {}
|
||||||
|
|
@ -5422,6 +5448,11 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- picomatch
|
- picomatch
|
||||||
|
|
||||||
|
svelte-echarts@1.0.0(echarts@5.6.0)(svelte@5.38.7):
|
||||||
|
dependencies:
|
||||||
|
echarts: 5.6.0
|
||||||
|
svelte: 5.38.7
|
||||||
|
|
||||||
svelte-eslint-parser@1.3.1(svelte@5.38.7):
|
svelte-eslint-parser@1.3.1(svelte@5.38.7):
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint-scope: 8.4.0
|
eslint-scope: 8.4.0
|
||||||
|
|
@ -5515,6 +5546,8 @@ snapshots:
|
||||||
|
|
||||||
ts-dedent@2.2.0: {}
|
ts-dedent@2.2.0: {}
|
||||||
|
|
||||||
|
tslib@2.3.0: {}
|
||||||
|
|
||||||
tslib@2.8.1: {}
|
tslib@2.8.1: {}
|
||||||
|
|
||||||
type-check@0.4.0:
|
type-check@0.4.0:
|
||||||
|
|
@ -5758,4 +5791,8 @@ snapshots:
|
||||||
|
|
||||||
zod@4.1.5: {}
|
zod@4.1.5: {}
|
||||||
|
|
||||||
|
zrender@5.6.1:
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.3.0
|
||||||
|
|
||||||
zwitch@2.0.4: {}
|
zwitch@2.0.4: {}
|
||||||
|
|
|
||||||
83
src/lib/components/charts/GwCrewBattleChart.svelte
Normal file
83
src/lib/components/charts/GwCrewBattleChart.svelte
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Chart } from 'svelte-echarts'
|
||||||
|
import { init, CHART_FONT_FAMILY } from './echarts-setup'
|
||||||
|
import { formatScore, formatScoreCompact } from '$lib/utils/gw'
|
||||||
|
import type { GwChartDataPoint } from '$lib/types/api/gw'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: GwChartDataPoint[]
|
||||||
|
height?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
let { data, height = 300 }: Props = $props()
|
||||||
|
|
||||||
|
const options = $derived({
|
||||||
|
textStyle: { fontFamily: CHART_FONT_FAMILY },
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis' as const,
|
||||||
|
formatter: (params: unknown) => {
|
||||||
|
const p = params as Array<{ seriesName: string; value: number; name: string }>
|
||||||
|
if (!p[0]) return ''
|
||||||
|
const crew = p.find((item) => item.seriesName === 'Our Crew')
|
||||||
|
const opp = p.find((item) => item.seriesName === 'Opponent')
|
||||||
|
return `${p[0].name}<br/>Our Crew: ${formatScore(crew?.value ?? 0)}<br/>Opponent: ${formatScore(opp?.value ?? 0)}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
data: ['Our Crew', 'Opponent'],
|
||||||
|
bottom: 0
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: 48,
|
||||||
|
right: 16,
|
||||||
|
top: 24,
|
||||||
|
bottom: 48
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category' as const,
|
||||||
|
data: data.map((d) => d.roundLabel),
|
||||||
|
axisLabel: { fontSize: 11 }
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value' as const,
|
||||||
|
axisLabel: {
|
||||||
|
formatter: (v: number) => formatScoreCompact(v),
|
||||||
|
fontSize: 11
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: 'Our Crew',
|
||||||
|
type: 'line' as const,
|
||||||
|
data: data.map((d) => d.crewScore),
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'circle',
|
||||||
|
symbolSize: 8,
|
||||||
|
lineStyle: { width: 2, color: '#2563eb' },
|
||||||
|
itemStyle: { color: '#2563eb' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Opponent',
|
||||||
|
type: 'line' as const,
|
||||||
|
data: data.map((d) => d.opponentScore),
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'diamond',
|
||||||
|
symbolSize: 8,
|
||||||
|
lineStyle: { width: 2, color: '#dc2626' },
|
||||||
|
itemStyle: { color: '#dc2626' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="chart-container" style:height="{height}px">
|
||||||
|
<Chart {init} {options} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.chart-container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
129
src/lib/components/charts/GwCrewHistoryChart.svelte
Normal file
129
src/lib/components/charts/GwCrewHistoryChart.svelte
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Chart } from 'svelte-echarts'
|
||||||
|
import { init, CHART_FONT_FAMILY } from './echarts-setup'
|
||||||
|
import { formatScore, formatScoreCompact, type HistoryDataPoint } from '$lib/utils/gw'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: HistoryDataPoint[]
|
||||||
|
height?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
let { data, height = 350 }: Props = $props()
|
||||||
|
|
||||||
|
// Calculate ~2 years of events (roughly 12 GWs per year = 24 events)
|
||||||
|
const defaultViewportSize = 24
|
||||||
|
const startPercent = $derived(
|
||||||
|
data.length > defaultViewportSize
|
||||||
|
? ((data.length - defaultViewportSize) / data.length) * 100
|
||||||
|
: 0
|
||||||
|
)
|
||||||
|
|
||||||
|
const options = $derived({
|
||||||
|
textStyle: { fontFamily: CHART_FONT_FAMILY },
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis' as const,
|
||||||
|
formatter: (params: unknown) => {
|
||||||
|
const p = params as Array<{ name: string; value: number; dataIndex: number }>
|
||||||
|
const point = p[0]
|
||||||
|
if (!point) return ''
|
||||||
|
const dataPoint = data[point.dataIndex]
|
||||||
|
return `${point.name}<br/>Score: ${formatScore(point.value)}<br/>${dataPoint?.date ?? ''}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toolbox: {
|
||||||
|
feature: {
|
||||||
|
dataZoom: {
|
||||||
|
yAxisIndex: 'none' as const,
|
||||||
|
title: { zoom: 'Zoom', back: 'Reset' }
|
||||||
|
},
|
||||||
|
restore: { title: 'Reset' }
|
||||||
|
},
|
||||||
|
right: 20
|
||||||
|
},
|
||||||
|
dataZoom: [
|
||||||
|
{
|
||||||
|
type: 'slider' as const,
|
||||||
|
xAxisIndex: 0,
|
||||||
|
start: startPercent,
|
||||||
|
end: 100,
|
||||||
|
height: 30,
|
||||||
|
bottom: 10,
|
||||||
|
borderColor: 'transparent',
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.05)',
|
||||||
|
fillerColor: 'rgba(37,99,235,0.2)',
|
||||||
|
handleStyle: { color: '#2563eb' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'inside' as const,
|
||||||
|
xAxisIndex: 0,
|
||||||
|
start: startPercent,
|
||||||
|
end: 100,
|
||||||
|
zoomOnMouseWheel: 'shift',
|
||||||
|
moveOnMouseMove: true,
|
||||||
|
moveOnMouseWheel: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
grid: {
|
||||||
|
left: 48,
|
||||||
|
right: 16,
|
||||||
|
bottom: 80,
|
||||||
|
top: 40
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category' as const,
|
||||||
|
data: data.map((d) => d.eventLabel),
|
||||||
|
axisLabel: {
|
||||||
|
rotate: 45,
|
||||||
|
interval: 'auto' as const,
|
||||||
|
fontSize: 11
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value' as const,
|
||||||
|
axisLabel: {
|
||||||
|
formatter: (v: number) => formatScoreCompact(v),
|
||||||
|
fontSize: 11
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
type: 'line' as const,
|
||||||
|
data: data.map((d) => d.totalScore),
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'circle',
|
||||||
|
symbolSize: 8,
|
||||||
|
lineStyle: { width: 2, color: '#2563eb' },
|
||||||
|
itemStyle: { color: '#2563eb' },
|
||||||
|
areaStyle: { opacity: 0.1, color: '#2563eb' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<div class="chart-container" style:height="{height}px">
|
||||||
|
<Chart {init} {options} />
|
||||||
|
</div>
|
||||||
|
<p class="chart-hint">Drag to pan • Shift+scroll to zoom • Use slider below chart</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use '$src/themes/typography' as typography;
|
||||||
|
|
||||||
|
.chart-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-hint {
|
||||||
|
text-align: center;
|
||||||
|
font-size: typography.$font-small;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
margin: 4px 0 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
124
src/lib/components/charts/GwMultiPlayerChart.svelte
Normal file
124
src/lib/components/charts/GwMultiPlayerChart.svelte
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Chart } from 'svelte-echarts'
|
||||||
|
import { init, CHART_FONT_FAMILY } from './echarts-setup'
|
||||||
|
import { formatScore, formatScoreCompact, type PlayerRoundScore } from '$lib/utils/gw'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
playerScores: Map<string, { name: string; scores: PlayerRoundScore[] }>
|
||||||
|
height?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
let { playerScores, height = 400 }: Props = $props()
|
||||||
|
|
||||||
|
// Color palette for 30 players
|
||||||
|
const colors = [
|
||||||
|
'#2563eb',
|
||||||
|
'#dc2626',
|
||||||
|
'#16a34a',
|
||||||
|
'#ca8a04',
|
||||||
|
'#9333ea',
|
||||||
|
'#0891b2',
|
||||||
|
'#ea580c',
|
||||||
|
'#db2777',
|
||||||
|
'#4f46e5',
|
||||||
|
'#65a30d',
|
||||||
|
'#0d9488',
|
||||||
|
'#be185d',
|
||||||
|
'#7c3aed',
|
||||||
|
'#b91c1c',
|
||||||
|
'#047857',
|
||||||
|
'#a16207',
|
||||||
|
'#6366f1',
|
||||||
|
'#c2410c',
|
||||||
|
'#0369a1',
|
||||||
|
'#4338ca',
|
||||||
|
'#15803d',
|
||||||
|
'#b45309',
|
||||||
|
'#7e22ce',
|
||||||
|
'#e11d48',
|
||||||
|
'#059669',
|
||||||
|
'#d97706',
|
||||||
|
'#8b5cf6',
|
||||||
|
'#f43f5e',
|
||||||
|
'#10b981',
|
||||||
|
'#f59e0b'
|
||||||
|
]
|
||||||
|
|
||||||
|
// Get round labels from first player
|
||||||
|
const roundLabels = $derived([...playerScores.values()][0]?.scores.map((s) => s.roundLabel) ?? [])
|
||||||
|
|
||||||
|
// Get player list for legend
|
||||||
|
const playerList = $derived([...playerScores.values()].map((p) => p.name))
|
||||||
|
|
||||||
|
// Default: top 5 players visible
|
||||||
|
const defaultSelected = $derived(
|
||||||
|
Object.fromEntries([...playerScores.values()].map((p, i) => [p.name, i < 5]))
|
||||||
|
)
|
||||||
|
|
||||||
|
const options = $derived({
|
||||||
|
textStyle: { fontFamily: CHART_FONT_FAMILY },
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis' as const,
|
||||||
|
formatter: (params: unknown) => {
|
||||||
|
const p = params as Array<{
|
||||||
|
marker: string
|
||||||
|
seriesName: string
|
||||||
|
value: number
|
||||||
|
name: string
|
||||||
|
}>
|
||||||
|
if (!p[0]) return ''
|
||||||
|
const sorted = [...p].sort((a, b) => b.value - a.value).slice(0, 10)
|
||||||
|
const lines = sorted.map(
|
||||||
|
(item) => `${item.marker} ${item.seriesName}: ${formatScore(item.value)}`
|
||||||
|
)
|
||||||
|
return `${p[0].name}<br/>${lines.join('<br/>')}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
type: 'scroll' as const,
|
||||||
|
bottom: 0,
|
||||||
|
data: playerList,
|
||||||
|
selected: defaultSelected
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: 48,
|
||||||
|
right: 16,
|
||||||
|
top: 24,
|
||||||
|
bottom: 80
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category' as const,
|
||||||
|
data: roundLabels,
|
||||||
|
axisLabel: { fontSize: 11 }
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value' as const,
|
||||||
|
axisLabel: {
|
||||||
|
formatter: (v: number) => formatScoreCompact(v),
|
||||||
|
fontSize: 11
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [...playerScores.entries()].map(([, player], i) => ({
|
||||||
|
name: player.name,
|
||||||
|
type: 'line' as const,
|
||||||
|
data: player.scores.map((s) => s.cumulative),
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'circle',
|
||||||
|
symbolSize: 6,
|
||||||
|
itemStyle: { color: colors[i % colors.length] },
|
||||||
|
lineStyle: { width: 2 }
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="chart-container" style:height="{height}px">
|
||||||
|
<Chart {init} {options} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.chart-container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
71
src/lib/components/charts/GwScoreLineChart.svelte
Normal file
71
src/lib/components/charts/GwScoreLineChart.svelte
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Chart } from 'svelte-echarts'
|
||||||
|
import { init, CHART_FONT_FAMILY } from './echarts-setup'
|
||||||
|
import { formatScore, formatScoreCompact, type PlayerRoundScore } from '$lib/utils/gw'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: PlayerRoundScore[]
|
||||||
|
title?: string
|
||||||
|
height?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
let { data, title, height = 300 }: Props = $props()
|
||||||
|
|
||||||
|
const options = $derived({
|
||||||
|
textStyle: { fontFamily: CHART_FONT_FAMILY },
|
||||||
|
title: title
|
||||||
|
? { text: title, left: 'center', textStyle: { fontSize: 14, fontFamily: CHART_FONT_FAMILY } }
|
||||||
|
: undefined,
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis' as const,
|
||||||
|
formatter: (params: unknown) => {
|
||||||
|
const p = params as Array<{ name: string; value: number }>
|
||||||
|
const point = p[0]
|
||||||
|
if (!point) return ''
|
||||||
|
return `${point.name}<br/>Score: ${formatScore(point.value)}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: 48,
|
||||||
|
right: 16,
|
||||||
|
top: title ? 40 : 24,
|
||||||
|
bottom: 40
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category' as const,
|
||||||
|
data: data.map((d) => d.roundLabel),
|
||||||
|
axisLabel: { rotate: 45, fontSize: 11 }
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value' as const,
|
||||||
|
axisLabel: {
|
||||||
|
formatter: (v: number) => formatScoreCompact(v),
|
||||||
|
fontSize: 11
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
type: 'line' as const,
|
||||||
|
data: data.map((d) => d.cumulative),
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'circle',
|
||||||
|
symbolSize: 8,
|
||||||
|
lineStyle: { width: 2, color: '#2563eb' },
|
||||||
|
itemStyle: { color: '#2563eb' },
|
||||||
|
areaStyle: { opacity: 0.1, color: '#2563eb' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="chart-container" style:height="{height}px">
|
||||||
|
<Chart {init} {options} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.chart-container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
35
src/lib/components/charts/echarts-setup.ts
Normal file
35
src/lib/components/charts/echarts-setup.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
/**
|
||||||
|
* ECharts Setup Module
|
||||||
|
*
|
||||||
|
* Configures tree-shaking for ECharts by only importing the components we need.
|
||||||
|
* This keeps the bundle size reasonable while providing full chart functionality.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { init, use } from 'echarts/core'
|
||||||
|
import { LineChart } from 'echarts/charts'
|
||||||
|
import {
|
||||||
|
GridComponent,
|
||||||
|
TooltipComponent,
|
||||||
|
LegendComponent,
|
||||||
|
TitleComponent,
|
||||||
|
DataZoomComponent,
|
||||||
|
ToolboxComponent
|
||||||
|
} from 'echarts/components'
|
||||||
|
import { CanvasRenderer } from 'echarts/renderers'
|
||||||
|
|
||||||
|
// Register only the components we need
|
||||||
|
use([
|
||||||
|
LineChart,
|
||||||
|
GridComponent,
|
||||||
|
TooltipComponent,
|
||||||
|
LegendComponent,
|
||||||
|
TitleComponent,
|
||||||
|
DataZoomComponent,
|
||||||
|
ToolboxComponent,
|
||||||
|
CanvasRenderer
|
||||||
|
])
|
||||||
|
|
||||||
|
// Shared font family matching the app's typography
|
||||||
|
export const CHART_FONT_FAMILY = "'AGrot', system-ui, sans-serif"
|
||||||
|
|
||||||
|
export { init }
|
||||||
Loading…
Reference in a new issue