1+ <script setup lang="ts">
2+ import { ref , onMounted , watch } from ' vue'
3+ import * as d3 from ' d3'
4+ import { useGameManager } from ' @/api/game/manager.ts'
5+
6+ const { gameState } = useGameManager ()
7+ const svgRef = ref <SVGSVGElement | null >(null )
8+ const tooltipRef = ref <HTMLDivElement | null >(null )
9+
10+ const getChartData = () => {
11+ const trip = gameState .value .trip
12+ if (! trip || trip .length === 0 ) return []
13+
14+ const typeCounts: Record <string , number > = {}
15+
16+ trip .forEach (segment => {
17+ const rawName = segment .details .route_short_name
18+ const type = rawName .replace (/ [0-9 \s ] / g , ' ' ) || " Other"
19+
20+ typeCounts [type ] = (typeCounts [type ] || 0 ) + 1
21+ })
22+
23+ return Object .entries (typeCounts )
24+ .map (([type , count ]) => ({ type , count }))
25+ .sort ((a , b ) => b .count - a .count )
26+ }
27+
28+ const drawChart = () => {
29+ const data = getChartData ()
30+ if (data .length === 0 || ! svgRef .value || ! tooltipRef .value ) return
31+
32+ const width = 500
33+ const height = 250
34+ const margin = 10
35+ const radius = (height / 2 ) - margin
36+
37+ const svg = d3 .select (svgRef .value )
38+ .attr (" viewBox" , ` 0 0 ${width } ${height } ` )
39+ .style (" width" , " 100%" )
40+ .style (" height" , " auto" )
41+
42+ svg .selectAll (" *" ).remove ()
43+ const tooltip = d3 .select (tooltipRef .value )
44+
45+ const donutX = width - radius - margin - 20
46+ const g = svg .append (" g" )
47+ .attr (" transform" , ` translate(${donutX },${height / 2 }) ` )
48+
49+ const color = d3 .scaleOrdinal ()
50+ .domain (data .map (d => d .type ))
51+ .range ([
52+ " var(--color-primary)" ,
53+ " var(--color-secondary)" ,
54+ " var(--color-accent)" ,
55+ " var(--color-info)" ,
56+ " var(--color-success)" ,
57+ " var(--color-warning)" ,
58+ " var(--color-neutral)"
59+ ])
60+
61+ const pie = d3 .pie <any >()
62+ .value (d => d .count )
63+ .sort (null )
64+
65+ const arc = d3 .arc <any >()
66+ .innerRadius (radius * 0.55 )
67+ .outerRadius (radius * 0.9 )
68+
69+ const arcHover = d3 .arc <any >()
70+ .innerRadius (radius * 0.55 )
71+ .outerRadius (radius * 0.98 )
72+
73+ const arcs = g .selectAll (" .arc" )
74+ .data (pie (data ))
75+ .enter ()
76+ .append (" g" )
77+ .attr (" class" , " arc" )
78+
79+ arcs .append (" path" )
80+ .attr (" d" , arc )
81+ .attr (" fill" , d => color (d .data .type ) as string )
82+ .attr (" stroke" , " var(--color-base-100)" )
83+ .style (" stroke-width" , " 3px" )
84+ .style (" cursor" , " pointer" )
85+ .on (" mouseover" , function (event , d ) {
86+ d3 .select (this ).transition ().duration (200 ).attr (" d" , arcHover )
87+ tooltip .style (" opacity" , 1 )
88+ .html (` <strong>${d .data .type }</strong><br/>${d .data .count } train(s) ` )
89+ })
90+ .on (" mousemove" , function (event ) {
91+ const [xPos, yPos] = d3 .pointer (event , svgRef .value )
92+ tooltip .style (" left" , (xPos - 110 ) + " px" )
93+ .style (" top" , (yPos - 20 ) + " px" )
94+ })
95+ .on (" mouseout" , function () {
96+ d3 .select (this ).transition ().duration (200 ).attr (" d" , arc )
97+ tooltip .style (" opacity" , 0 )
98+ })
99+
100+ const legendSpacing = 24
101+ const totalLegendHeight = data .length * legendSpacing
102+ const startY = (height - totalLegendHeight ) / 2 + 10
103+
104+ const legend = svg .append (" g" )
105+ .attr (" transform" , ` translate(10, ${startY }) ` )
106+
107+ const legendItems = legend .selectAll (" .legend-item" )
108+ .data (data )
109+ .enter ()
110+ .append (" g" )
111+ .attr (" class" , " legend-item" )
112+ .attr (" transform" , (d , i ) => ` translate(0, ${i * legendSpacing }) ` )
113+
114+ legendItems .append (" rect" )
115+ .attr (" width" , 16 )
116+ .attr (" height" , 16 )
117+ .attr (" fill" , d => color (d .type ) as string )
118+ .attr (" rx" , 4 )
119+
120+ legendItems .append (" text" )
121+ .attr (" x" , 28 )
122+ .attr (" y" , 13 )
123+ .attr (" fill" , " currentColor" )
124+ .style (" font-size" , " 14px" )
125+ .style (" font-weight" , " 500" )
126+ .text (d => ` ${d .type } (${d .count }) ` )
127+ }
128+
129+ watch (() => gameState .value .trip , drawChart , { deep: true })
130+ onMounted (drawChart )
131+ </script >
132+
133+ <template >
134+ <div class =" relative w-full h-full flex justify-center items-center py-2" >
135+ <svg ref =" svgRef" class =" text-base-content w-full" ></svg >
136+
137+ <div
138+ ref =" tooltipRef"
139+ class =" absolute opacity-0 bg-neutral text-neutral-content px-3 py-2 rounded-box shadow-lg pointer-events-none text-sm transition-opacity duration-200 z-[100] text-left"
140+ ></div >
141+ </div >
142+ </template >
0 commit comments