Skip to content

Commit 1b9bf67

Browse files
AlexandreMaquetylked
authored andcommitted
Implement train type used viz
1 parent 5248155 commit 1b9bf67

1 file changed

Lines changed: 142 additions & 0 deletions

File tree

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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

Comments
 (0)