2nzi commited on
Commit
62b5f3c
·
verified ·
1 Parent(s): 49acde2

Plotly chart

Browse files
package-lock.json CHANGED
@@ -8,7 +8,9 @@
8
  "name": "soccer-field-app",
9
  "version": "0.0.0",
10
  "dependencies": {
 
11
  "pinia": "^3.0.1",
 
12
  "vue": "^3.5.13",
13
  "vue-router": "^4.5.0"
14
  },
@@ -2066,6 +2068,15 @@
2066
  "dev": true,
2067
  "license": "MIT"
2068
  },
 
 
 
 
 
 
 
 
 
2069
  "node_modules/concat-map": {
2070
  "version": "0.0.1",
2071
  "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -2129,6 +2140,31 @@
2129
  "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
2130
  "license": "MIT"
2131
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2132
  "node_modules/debug": {
2133
  "version": "4.4.1",
2134
  "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
@@ -2773,6 +2809,18 @@
2773
  "node": ">=18.18.0"
2774
  }
2775
  },
 
 
 
 
 
 
 
 
 
 
 
 
2776
  "node_modules/ignore": {
2777
  "version": "5.3.2",
2778
  "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -3382,6 +3430,12 @@
3382
  }
3383
  }
3384
  },
 
 
 
 
 
 
3385
  "node_modules/postcss": {
3386
  "version": "8.5.6",
3387
  "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@@ -3565,6 +3619,18 @@
3565
  "url": "https://github.com/sponsors/sindresorhus"
3566
  }
3567
  },
 
 
 
 
 
 
 
 
 
 
 
 
3568
  "node_modules/semver": {
3569
  "version": "7.7.2",
3570
  "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
 
8
  "name": "soccer-field-app",
9
  "version": "0.0.0",
10
  "dependencies": {
11
+ "d3-dsv": "^3.0.1",
12
  "pinia": "^3.0.1",
13
+ "plotly.js-dist": "^2.29.1",
14
  "vue": "^3.5.13",
15
  "vue-router": "^4.5.0"
16
  },
 
2068
  "dev": true,
2069
  "license": "MIT"
2070
  },
2071
+ "node_modules/commander": {
2072
+ "version": "7.2.0",
2073
+ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
2074
+ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
2075
+ "license": "MIT",
2076
+ "engines": {
2077
+ "node": ">= 10"
2078
+ }
2079
+ },
2080
  "node_modules/concat-map": {
2081
  "version": "0.0.1",
2082
  "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
 
2140
  "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
2141
  "license": "MIT"
2142
  },
2143
+ "node_modules/d3-dsv": {
2144
+ "version": "3.0.1",
2145
+ "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
2146
+ "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
2147
+ "license": "ISC",
2148
+ "dependencies": {
2149
+ "commander": "7",
2150
+ "iconv-lite": "0.6",
2151
+ "rw": "1"
2152
+ },
2153
+ "bin": {
2154
+ "csv2json": "bin/dsv2json.js",
2155
+ "csv2tsv": "bin/dsv2dsv.js",
2156
+ "dsv2dsv": "bin/dsv2dsv.js",
2157
+ "dsv2json": "bin/dsv2json.js",
2158
+ "json2csv": "bin/json2dsv.js",
2159
+ "json2dsv": "bin/json2dsv.js",
2160
+ "json2tsv": "bin/json2dsv.js",
2161
+ "tsv2csv": "bin/dsv2dsv.js",
2162
+ "tsv2json": "bin/dsv2json.js"
2163
+ },
2164
+ "engines": {
2165
+ "node": ">=12"
2166
+ }
2167
+ },
2168
  "node_modules/debug": {
2169
  "version": "4.4.1",
2170
  "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
 
2809
  "node": ">=18.18.0"
2810
  }
2811
  },
2812
+ "node_modules/iconv-lite": {
2813
+ "version": "0.6.3",
2814
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
2815
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
2816
+ "license": "MIT",
2817
+ "dependencies": {
2818
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
2819
+ },
2820
+ "engines": {
2821
+ "node": ">=0.10.0"
2822
+ }
2823
+ },
2824
  "node_modules/ignore": {
2825
  "version": "5.3.2",
2826
  "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
 
3430
  }
3431
  }
3432
  },
3433
+ "node_modules/plotly.js-dist": {
3434
+ "version": "2.35.3",
3435
+ "resolved": "https://registry.npmjs.org/plotly.js-dist/-/plotly.js-dist-2.35.3.tgz",
3436
+ "integrity": "sha512-dqB9+FUyBFZN04xWnZoYwaeeF4Jj9T/m0CHYmoozmPC3R4Dy0TRJsHgbRVLPxgYQqodzniVUj17+2wmJuGaZAg==",
3437
+ "license": "MIT"
3438
+ },
3439
  "node_modules/postcss": {
3440
  "version": "8.5.6",
3441
  "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
 
3619
  "url": "https://github.com/sponsors/sindresorhus"
3620
  }
3621
  },
3622
+ "node_modules/rw": {
3623
+ "version": "1.3.3",
3624
+ "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
3625
+ "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
3626
+ "license": "BSD-3-Clause"
3627
+ },
3628
+ "node_modules/safer-buffer": {
3629
+ "version": "2.1.2",
3630
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
3631
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
3632
+ "license": "MIT"
3633
+ },
3634
  "node_modules/semver": {
3635
  "version": "7.7.2",
3636
  "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
package.json CHANGED
@@ -13,7 +13,9 @@
13
  "dependencies": {
14
  "pinia": "^3.0.1",
15
  "vue": "^3.5.13",
16
- "vue-router": "^4.5.0"
 
 
17
  },
18
  "devDependencies": {
19
  "@eslint/js": "^9.22.0",
 
13
  "dependencies": {
14
  "pinia": "^3.0.1",
15
  "vue": "^3.5.13",
16
+ "vue-router": "^4.5.0",
17
+ "plotly.js-dist": "^2.29.1",
18
+ "d3-dsv": "^3.0.1"
19
  },
20
  "devDependencies": {
21
  "@eslint/js": "^9.22.0",
src/components/PlotlyChart.vue ADDED
@@ -0,0 +1,675 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <div class="plotly-chart">
3
+ <div class="chart-header">
4
+ <h3>{{ title }}</h3>
5
+ <button v-if="loading" class="refresh-btn" disabled>
6
+ Chargement...
7
+ </button>
8
+
9
+ </div>
10
+
11
+ <div
12
+ ref="plotlyDiv"
13
+ :id="chartId"
14
+ class="chart-container"
15
+ :class="{ loading: loading }"
16
+ >
17
+ <div v-if="loading" class="loading-spinner">
18
+ <div class="spinner"></div>
19
+ <p>Chargement des données...</p>
20
+ </div>
21
+
22
+ </div>
23
+ </div>
24
+ </template>
25
+
26
+ <script>
27
+ import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue'
28
+ import Plotly from 'plotly.js-dist'
29
+ import { csvParse } from 'd3-dsv'
30
+
31
+ export default {
32
+ name: 'PlotlyChart',
33
+ props: {
34
+ title: {
35
+ type: String,
36
+ default: ''
37
+ },
38
+ csvUrl: {
39
+ type: String,
40
+ default: ''
41
+ },
42
+ dataType: {
43
+ type: String,
44
+ default: 'csv', // 'csv' ou 'football-field'
45
+ validator: (value) => ['csv', 'football-field'].includes(value)
46
+ },
47
+ chartId: {
48
+ type: String,
49
+ default: () => ``
50
+ },
51
+ height: {
52
+ type: [String, Number],
53
+ default: 500
54
+ },
55
+ customLayout: {
56
+ type: Object,
57
+ default: () => ({})
58
+ },
59
+ keypointsData: {
60
+ type: Array,
61
+ default: () => []
62
+ },
63
+ cameraParams: {
64
+ type: Object,
65
+ default: () => null
66
+ }
67
+ },
68
+ emits: ['data-loaded', 'error'],
69
+ setup(props, { emit }) {
70
+ const plotlyDiv = ref(null)
71
+ const loading = ref(false)
72
+ const error = ref(null)
73
+ const chartData = ref([])
74
+
75
+ // Fonction pour décompresser les données du CSV
76
+ const unpack = (rows, key) => {
77
+ return rows.map(row => row[key])
78
+ }
79
+
80
+ // Fonction pour charger les données depuis le CSV
81
+ const loadData = async () => {
82
+ loading.value = true
83
+ error.value = null
84
+
85
+ try {
86
+ if (props.dataType === 'football-field') {
87
+ // Générer les données du terrain de football
88
+ await generateFootballFieldData()
89
+ } else {
90
+ // Charger depuis CSV (comportement original)
91
+ await loadCsvData()
92
+ }
93
+
94
+ await renderChart()
95
+ emit('data-loaded', { traces: chartData.value })
96
+
97
+ } catch (err) {
98
+ console.error('Erreur lors du chargement des données:', err)
99
+ error.value = err.message
100
+ emit('error', err)
101
+ } finally {
102
+ loading.value = false
103
+ }
104
+ }
105
+
106
+ // Fonction pour charger les données CSV (ancien comportement)
107
+ const loadCsvData = async () => {
108
+ const response = await fetch(props.csvUrl)
109
+ if (!response.ok) {
110
+ throw new Error(`HTTP error! status: ${response.status}`)
111
+ }
112
+
113
+ const csvText = await response.text()
114
+ const rows = csvParse(csvText)
115
+
116
+ if (!rows || rows.length === 0) {
117
+ throw new Error('Aucune donnée trouvée dans le CSV')
118
+ }
119
+
120
+ // Création des traces comme dans votre exemple
121
+ const trace1 = {
122
+ x: unpack(rows, 'x1'),
123
+ y: unpack(rows, 'y1'),
124
+ z: unpack(rows, 'z1'),
125
+ mode: 'markers',
126
+ marker: {
127
+ size: 12,
128
+ line: {
129
+ color: 'rgba(217, 217, 217, 0.14)',
130
+ width: 0.5
131
+ },
132
+ opacity: 0.8
133
+ },
134
+ type: 'scatter3d',
135
+ name: 'Série 1'
136
+ }
137
+
138
+ const trace2 = {
139
+ x: unpack(rows, 'x2'),
140
+ y: unpack(rows, 'y2'),
141
+ z: unpack(rows, 'z2'),
142
+ mode: 'markers',
143
+ marker: {
144
+ color: 'rgb(127, 127, 127)',
145
+ size: 12,
146
+ symbol: 'circle',
147
+ line: {
148
+ color: 'rgb(204, 204, 204)',
149
+ width: 1
150
+ },
151
+ opacity: 0.8
152
+ },
153
+ type: 'scatter3d',
154
+ name: 'Série 2'
155
+ }
156
+
157
+ chartData.value = [trace1, trace2]
158
+ }
159
+
160
+ // Fonction pour générer les données du terrain de football
161
+ const generateFootballFieldData = async () => {
162
+ const fieldLength = 105 // mètres
163
+ const fieldWidth = 68 // mètres
164
+
165
+ const traces = []
166
+
167
+ // 1. Contour principal du terrain
168
+ const fieldCorners = {
169
+ x: [-fieldLength/2, fieldLength/2, fieldLength/2, -fieldLength/2, -fieldLength/2],
170
+ y: [-fieldWidth/2, -fieldWidth/2, fieldWidth/2, fieldWidth/2, -fieldWidth/2],
171
+ z: [0, 0, 0, 0, 0],
172
+ mode: 'lines',
173
+ type: 'scatter3d',
174
+ line: { color: '#00FF00', width: 6 },
175
+ name: 'Terrain principal',
176
+ showlegend: true
177
+ }
178
+ traces.push(fieldCorners)
179
+
180
+ // 2. Ligne médiane
181
+ const midline = {
182
+ x: [0, 0],
183
+ y: [-fieldWidth/2, fieldWidth/2],
184
+ z: [0, 0],
185
+ mode: 'lines',
186
+ type: 'scatter3d',
187
+ line: { color: 'bleu', width: 4 },
188
+ name: 'Ligne médiane',
189
+ showlegend: true
190
+ }
191
+ traces.push(midline)
192
+
193
+ // 3. Surface de réparation gauche
194
+ const leftPenaltyArea = {
195
+ x: [-fieldLength/2, -fieldLength/2+16.5, -fieldLength/2+16.5, -fieldLength/2, -fieldLength/2],
196
+ y: [-20.16, -20.16, 20.16, 20.16, -20.16],
197
+ z: [0, 0, 0, 0, 0],
198
+ mode: 'lines',
199
+ type: 'scatter3d',
200
+ line: { color: '#FF6B6B', width: 5 },
201
+ name: 'Surface de réparation',
202
+ showlegend: true
203
+ }
204
+ traces.push(leftPenaltyArea)
205
+
206
+ // 4. Surface de réparation droite
207
+ const rightPenaltyArea = {
208
+ x: [fieldLength/2, fieldLength/2-16.5, fieldLength/2-16.5, fieldLength/2, fieldLength/2],
209
+ y: [-20.16, -20.16, 20.16, 20.16, -20.16],
210
+ z: [0, 0, 0, 0, 0],
211
+ mode: 'lines',
212
+ type: 'scatter3d',
213
+ line: { color: '#FF6B6B', width: 5 },
214
+ name: 'Surface de réparation droite',
215
+ showlegend: false // Eviter la duplication dans la légende
216
+ }
217
+ traces.push(rightPenaltyArea)
218
+
219
+ // 5. Surface de but gauche (6 yards)
220
+ const leftGoalArea = {
221
+ x: [-fieldLength/2, -fieldLength/2+5.5, -fieldLength/2+5.5, -fieldLength/2, -fieldLength/2],
222
+ y: [-9.16, -9.16, 9.16, 9.16, -9.16],
223
+ z: [0, 0, 0, 0, 0],
224
+ mode: 'lines',
225
+ type: 'scatter3d',
226
+ line: { color: '#4ECDC4', width: 4 },
227
+ name: 'Surface de but',
228
+ showlegend: true
229
+ }
230
+ traces.push(leftGoalArea)
231
+
232
+ // 6. Surface de but droite
233
+ const rightGoalArea = {
234
+ x: [fieldLength/2, fieldLength/2-5.5, fieldLength/2-5.5, fieldLength/2, fieldLength/2],
235
+ y: [-9.16, -9.16, 9.16, 9.16, -9.16],
236
+ z: [0, 0, 0, 0, 0],
237
+ mode: 'lines',
238
+ type: 'scatter3d',
239
+ line: { color: '#4ECDC4', width: 4 },
240
+ showlegend: false
241
+ }
242
+ traces.push(rightGoalArea)
243
+
244
+ // 7. Cercle central
245
+ const circlePoints = 50
246
+ const radius = 9.15
247
+ const theta = Array.from({length: circlePoints}, (_, i) => (i / (circlePoints - 1)) * 2 * Math.PI)
248
+ const centerCircle = {
249
+ x: theta.map(t => radius * Math.cos(t)),
250
+ y: theta.map(t => radius * Math.sin(t)),
251
+ z: theta.map(() => 0),
252
+ mode: 'lines',
253
+ type: 'scatter3d',
254
+ line: { color: '#FFE66D', width: 4 },
255
+ name: 'Cercle central',
256
+ showlegend: true
257
+ }
258
+ traces.push(centerCircle)
259
+
260
+ // 8. Points de penalty
261
+ const penaltySpots = {
262
+ x: [-fieldLength/2 + 11, fieldLength/2 - 11],
263
+ y: [0, 0],
264
+ z: [0, 0],
265
+ mode: 'markers',
266
+ type: 'scatter3d',
267
+ marker: {
268
+ color: '#FFFFFF',
269
+ size: 8,
270
+ symbol: 'circle'
271
+ },
272
+ name: 'Points de penalty',
273
+ showlegend: true
274
+ }
275
+ traces.push(penaltySpots)
276
+
277
+ // 9. Point central
278
+ const centerSpot = {
279
+ x: [0],
280
+ y: [0],
281
+ z: [0],
282
+ mode: 'markers',
283
+ type: 'scatter3d',
284
+ marker: {
285
+ color: '#FFFFFF',
286
+ size: 6,
287
+ symbol: 'circle'
288
+ },
289
+ name: 'Point central',
290
+ showlegend: true
291
+ }
292
+ traces.push(centerSpot)
293
+
294
+ // 10. Keypoints détectés (si disponibles)
295
+ if (props.keypointsData && props.keypointsData.length > 0) {
296
+ const keypointsTrace = {
297
+ x: props.keypointsData.map(kp => kp.world_coords?.x || 0),
298
+ y: props.keypointsData.map(kp => kp.world_coords?.y || 0),
299
+ z: props.keypointsData.map(() => 0.5), // Légèrement au-dessus du terrain
300
+ mode: 'markers+text',
301
+ type: 'scatter3d',
302
+ marker: {
303
+ color: '#FF1744',
304
+ size: 6,
305
+ symbol: 'circle',
306
+ line: {
307
+ color: '#FFFFFF',
308
+ width: 2
309
+ }
310
+ },
311
+ text: props.keypointsData.map(kp => `KP ${kp.id}`),
312
+ textposition: 'top center',
313
+ textfont: {
314
+ color: 'black',
315
+ size: 8,
316
+ family: 'Arial, sans-serif'
317
+ },
318
+ name: 'Keypoints détectés',
319
+ showlegend: false,
320
+ hovertemplate:
321
+ '<b>Keypoint %{text}</b><br>' +
322
+ 'X: %{x:.1f}m<br>' +
323
+ 'Y: %{y:.1f}m<br>' +
324
+ '<extra></extra>'
325
+ }
326
+ traces.push(keypointsTrace)
327
+ }
328
+
329
+ // 11. Position de la caméra (si disponible)
330
+ if (props.cameraParams?.position_meters) {
331
+ const [camX, camY, camZ] = props.cameraParams.position_meters
332
+
333
+ const cameraTrace = {
334
+ x: [camX],
335
+ y: [camY],
336
+ z: [camZ],
337
+ mode: 'markers+text',
338
+ type: 'scatter3d',
339
+ marker: {
340
+ color: '#2196F3',
341
+ size: 15,
342
+ symbol: 'square',
343
+ line: {
344
+ color: '#FFFFFF',
345
+ width: 3
346
+ }
347
+ },
348
+ text: ['📷 Caméra'],
349
+ textposition: 'top center',
350
+ textfont: {
351
+ color: '#2196F3',
352
+ size: 12,
353
+ family: 'Arial, sans-serif'
354
+ },
355
+ name: 'Position caméra',
356
+ showlegend: false,
357
+ hovertemplate:
358
+ '<b>📷 Position de la caméra</b><br>' +
359
+ 'X: %{x:.2f}m<br>' +
360
+ 'Y: %{y:.2f}m<br>' +
361
+ 'Z: %{z:.2f}m<br>' +
362
+ '<extra></extra>'
363
+ }
364
+ traces.push(cameraTrace)
365
+
366
+ // 12. Ligne de vue de la caméra vers le centre du terrain (optionnel)
367
+ const sightLineTrace = {
368
+ x: [camX, 0],
369
+ y: [camY, 0],
370
+ z: [camZ, 0],
371
+ mode: 'lines',
372
+ type: 'scatter3d',
373
+ line: {
374
+ color: '#2196F3',
375
+ width: 2,
376
+ dash: 'dot'
377
+ },
378
+ name: 'Ligne de vue',
379
+ showlegend: false,
380
+ hoverinfo: 'skip'
381
+ }
382
+ traces.push(sightLineTrace)
383
+ }
384
+
385
+ chartData.value = traces
386
+ }
387
+
388
+ // Fonction pour rendre le graphique
389
+ const renderChart = async () => {
390
+ if (!plotlyDiv.value || chartData.value.length === 0) return
391
+
392
+ let defaultLayout = {
393
+ margin: {
394
+ l: 0,
395
+ r: 0,
396
+ b: 0,
397
+ t: 0
398
+ },
399
+ height: typeof props.height === 'number' ? props.height : parseInt(props.height),
400
+ scene: {
401
+ xaxis: { title: 'X Axis' },
402
+ yaxis: { title: 'Y Axis' },
403
+ zaxis: { title: 'Z Axis' }
404
+ }
405
+ }
406
+
407
+ // Layout spécifique pour le terrain de football
408
+ if (props.dataType === 'football-field') {
409
+ const shift_l = 10;
410
+ const shift_w = 40;
411
+
412
+ // Ranges de base
413
+ let baseLength = 52.5 + shift_l; // range X: [-baseLength, baseLength]
414
+ let baseWidth = 34 + shift_w; // range Y: [-baseWidth, baseWidth]
415
+ let baseHeight = 35; // range Z: [-baseHeight, baseHeight]
416
+
417
+ // Position de la caméra
418
+ const camX = props.cameraParams?.position_meters?.[0] || 0;
419
+ const camY = props.cameraParams?.position_meters?.[1] || 0;
420
+ const camZ = props.cameraParams?.position_meters?.[2] || 0;
421
+
422
+ // Vérifier si la caméra dépasse les ranges et ajuster si nécessaire
423
+ const maxCamX = Math.abs(camX);
424
+ const maxCamY = Math.abs(camY);
425
+ const maxCamZ = Math.abs(camZ);
426
+
427
+ // Ratios actuels pour conserver les proportions
428
+ const ratioXY = baseLength / baseWidth; // ratio X/Y
429
+ const ratioXZ = baseLength / baseHeight; // ratio X/Z
430
+ const ratioYZ = baseWidth / baseHeight; // ratio Y/Z
431
+
432
+ // Ajuster les ranges si la caméra dépasse
433
+ if (maxCamX > baseLength) {
434
+ baseLength = maxCamX + 10; // marge de 10m
435
+ baseWidth = baseLength / ratioXY; // conserver ratio X/Y
436
+ baseHeight = baseLength / ratioXZ; // conserver ratio X/Z
437
+ }
438
+
439
+ if (maxCamY > baseWidth) {
440
+ baseWidth = maxCamY + 10; // marge de 10m
441
+ baseLength = baseWidth * ratioXY; // conserver ratio X/Y
442
+ baseHeight = baseWidth / ratioYZ; // conserver ratio Y/Z
443
+ }
444
+
445
+ if (maxCamZ > baseHeight) {
446
+ baseHeight = maxCamZ + 10; // marge de 10m
447
+ baseLength = baseHeight * ratioXZ; // conserver ratio X/Z
448
+ baseWidth = baseHeight * ratioYZ; // conserver ratio Y/Z
449
+ }
450
+
451
+ // Valeurs finales
452
+ const length = baseLength;
453
+ const witdh = baseWidth;
454
+ const height = baseHeight;
455
+
456
+ defaultLayout.scene = {
457
+ xaxis: {
458
+ title: '',
459
+ range: [-length, length],
460
+ showgrid: false,
461
+ showticklabels: false,
462
+ showline: false,
463
+ zeroline: false,
464
+ dtick: 20
465
+ },
466
+ yaxis: {
467
+ title: '',
468
+ range: [-witdh, witdh],
469
+ showgrid: false,
470
+ showticklabels: false,
471
+ showline: false,
472
+ zeroline: false,
473
+ dtick: 20
474
+ },
475
+ zaxis: {
476
+ title: '',
477
+ range: [-height, height],
478
+ showgrid: false,
479
+ showticklabels: false,
480
+ showline: false,
481
+ zeroline: false,
482
+ dtick: 0
483
+ },
484
+ aspectmode: 'manual',
485
+ aspectratio: { x: 1., y: 1, z: 0.3 },
486
+ camera: {
487
+ eye: { x: 0, y: 1, z: -0.6 },
488
+ center: { x: 0, y: 0, z: 0 },
489
+ up: { x: 0, y: -1, z: 0 }
490
+ }
491
+ }
492
+ defaultLayout.margin.t = 10
493
+ defaultLayout.showlegend = false
494
+ }
495
+
496
+ const layout = {
497
+ ...defaultLayout,
498
+ ...props.customLayout
499
+ }
500
+
501
+ const config = {
502
+ responsive: true,
503
+ displayModeBar: true,
504
+ modeBarButtonsToRemove: ['pan2d', 'lasso2d'],
505
+ displaylogo: false
506
+ }
507
+
508
+ await nextTick()
509
+ await Plotly.newPlot(plotlyDiv.value, chartData.value, layout, config)
510
+ }
511
+
512
+ // Fonction de nettoyage
513
+ const cleanup = () => {
514
+ if (plotlyDiv.value) {
515
+ Plotly.purge(plotlyDiv.value)
516
+ }
517
+ }
518
+
519
+ // Fonction pour redimensionner le graphique
520
+ const resizeChart = () => {
521
+ if (plotlyDiv.value) {
522
+ Plotly.Plots.resize(plotlyDiv.value)
523
+ }
524
+ }
525
+
526
+ // Watcher pour recharger quand les keypoints changent
527
+ watch(() => props.keypointsData, () => {
528
+ if (props.dataType === 'football-field') {
529
+ loadData()
530
+ }
531
+ }, { deep: true })
532
+
533
+ // Watcher pour recharger quand les paramètres de la caméra changent
534
+ watch(() => props.cameraParams, () => {
535
+ if (props.dataType === 'football-field') {
536
+ loadData()
537
+ }
538
+ }, { deep: true })
539
+
540
+ // Lifecycle hooks
541
+ onMounted(() => {
542
+ loadData()
543
+ window.addEventListener('resize', resizeChart)
544
+ })
545
+
546
+ onUnmounted(() => {
547
+ cleanup()
548
+ window.removeEventListener('resize', resizeChart)
549
+ })
550
+
551
+ return {
552
+ plotlyDiv,
553
+ loading,
554
+ error,
555
+ loadData,
556
+ resizeChart
557
+ }
558
+ }
559
+ }
560
+ </script>
561
+
562
+ <style scoped>
563
+ .plotly-chart {
564
+ background: white;
565
+ border-radius: 8px;
566
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
567
+ overflow: hidden;
568
+ }
569
+
570
+
571
+
572
+ .chart-container {
573
+ position: relative;
574
+ min-height: 200px;
575
+ }
576
+
577
+ .chart-container.loading {
578
+ display: flex;
579
+ align-items: center;
580
+ justify-content: center;
581
+ }
582
+
583
+ .loading-spinner {
584
+ display: flex;
585
+ flex-direction: column;
586
+ align-items: center;
587
+ gap: 1rem;
588
+ }
589
+
590
+ .spinner {
591
+ width: 40px;
592
+ height: 40px;
593
+ border: 4px solid #f3f3f3;
594
+ border-top: 4px solid #007bff;
595
+ border-radius: 50%;
596
+ animation: spin 1s linear infinite;
597
+ }
598
+
599
+ @keyframes spin {
600
+ 0% { transform: rotate(0deg); }
601
+ 100% { transform: rotate(360deg); }
602
+ }
603
+
604
+ .error-message {
605
+ text-align: center;
606
+ padding: 2rem;
607
+ color: #dc3545;
608
+ }
609
+
610
+ .retry-btn {
611
+ background: #dc3545;
612
+ color: white;
613
+ border: none;
614
+ padding: 0.5rem 1rem;
615
+ border-radius: 4px;
616
+ cursor: pointer;
617
+ margin-top: 1rem;
618
+ }
619
+
620
+ .retry-btn:hover {
621
+ background: #c82333;
622
+ }
623
+
624
+ /* Correction de l'alignement de la modebar Plotly */
625
+ .plotly-chart :deep(.modebar) {
626
+ display: flex !important;
627
+ align-items: center !important;
628
+ justify-content: flex-end !important;
629
+ padding: 5px !important;
630
+ background: none !important;
631
+ }
632
+
633
+ .plotly-chart :deep(.modebar-group) {
634
+ display: flex !important;
635
+ align-items: center !important;
636
+ margin: 0 2px !important;
637
+ background: none !important;
638
+ border: none !important;
639
+ }
640
+
641
+ .plotly-chart :deep(.modebar-btn) {
642
+ display: flex !important;
643
+ align-items: center !important;
644
+ justify-content: center !important;
645
+ margin: 0 1px !important;
646
+ background: none !important;
647
+ border: none !important;
648
+ }
649
+
650
+ /* Personnalisation des icônes */
651
+ .plotly-chart :deep(.modebar-btn .icon path) {
652
+ fill: black !important;
653
+ }
654
+
655
+ .plotly-chart :deep(.modebar-btn:hover) {
656
+ background: rgba(0, 0, 0, 0.1) !important;
657
+ }
658
+
659
+ .plotly-chart :deep(.modebar-btn.active) {
660
+ background: rgba(0, 0, 0, 0.2) !important;
661
+ }
662
+
663
+ /* Responsive */
664
+ @media (max-width: 768px) {
665
+ .chart-header {
666
+ flex-direction: column;
667
+ gap: 0.5rem;
668
+ text-align: center;
669
+ }
670
+
671
+ .chart-container {
672
+ min-height: 300px;
673
+ }
674
+ }
675
+ </style>
src/services/api.js CHANGED
@@ -1,5 +1,5 @@
1
- // const API_BASE_URL = 'http://localhost:8000'
2
- const API_BASE_URL = 'https://2nzi-pnlcalib.hf.space'
3
 
4
  class FootballVisionAPI {
5
  constructor() {
 
1
+ const API_BASE_URL = 'http://localhost:8000'
2
+ // const API_BASE_URL = 'https://2nzi-pnlcalib.hf.space'
3
 
4
  class FootballVisionAPI {
5
  constructor() {
src/views/HomeView.vue CHANGED
@@ -6,6 +6,7 @@ import { useUploadStore } from '../stores/upload'
6
  import api from '../services/api'
7
  import CalibrationArea from '@/components/CalibrationArea.vue'
8
  import FootballField from '@/components/FootballField.vue'
 
9
 
10
  const router = useRouter()
11
  const calibrationStore = useCalibrationStore()
@@ -525,10 +526,28 @@ const processCalibration = async () => {
525
  })
526
  }
527
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
528
  </script>
529
 
530
  <template>
531
- <div class="home-container">
 
 
 
532
  <!-- Bouton retour subtil (visible si on a déjà fait des actions) -->
533
  <button
534
  v-if="showUpload || showResults || showError"
@@ -858,6 +877,27 @@ const processCalibration = async () => {
858
  </button>
859
  </div>
860
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
861
  <details class="result-details">
862
  <summary>Données complètes</summary>
863
  <pre class="result-data">{{ JSON.stringify(calibrationStore.results, null, 2) }}</pre>
@@ -1454,7 +1494,7 @@ const processCalibration = async () => {
1454
 
1455
  /* Section Résultats */
1456
  .results-container {
1457
- max-width: 600px;
1458
  width: 100%;
1459
  display: flex;
1460
  flex-direction: column;
@@ -1560,19 +1600,27 @@ const processCalibration = async () => {
1560
  }
1561
 
1562
  .result-data {
1563
- background: #1a1a1a;
1564
- color: #e0e0e0;
1565
- padding: 1rem;
1566
- border-radius: 6px;
1567
- font-family: 'Courier New', monospace;
1568
- font-size: 0.8rem;
1569
- line-height: 1.4;
1570
  overflow-x: auto;
1571
- white-space: pre-wrap;
1572
- word-wrap: break-word;
1573
- border: 1px solid #333;
1574
- max-height: 300px;
1575
  overflow-y: auto;
 
 
 
 
 
 
 
 
 
1576
  }
1577
 
1578
  .confidence-score {
@@ -1807,6 +1855,25 @@ const processCalibration = async () => {
1807
  height: calc(100% - 80px);
1808
  }
1809
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1810
  /* Responsive */
1811
  @media (max-width: 768px) {
1812
  .home-container {
@@ -1833,5 +1900,9 @@ const processCalibration = async () => {
1833
  flex-direction: column;
1834
  }
1835
 
 
 
 
 
1836
  }
1837
  </style>
 
6
  import api from '../services/api'
7
  import CalibrationArea from '@/components/CalibrationArea.vue'
8
  import FootballField from '@/components/FootballField.vue'
9
+ import PlotlyChart from '@/components/PlotlyChart.vue'
10
 
11
  const router = useRouter()
12
  const calibrationStore = useCalibrationStore()
 
526
  })
527
  }
528
  }
529
+
530
+ // Méthodes pour les événements du graphique Plotly
531
+ const onChartDataLoaded = (data) => {
532
+ if (data.rows) {
533
+ // Pour les données CSV
534
+ console.log('📊 Données CSV chargées:', data.rows.length, 'points')
535
+ } else {
536
+ // Pour le terrain de football ou autres
537
+ console.log('📊 Graphique chargé:', data.traces.length, 'éléments')
538
+ }
539
+ }
540
+
541
+ const onChartError = (error) => {
542
+ console.error('❌ Erreur dans le graphique:', error)
543
+ }
544
  </script>
545
 
546
  <template>
547
+
548
+
549
+
550
+ <div class="home-container">
551
  <!-- Bouton retour subtil (visible si on a déjà fait des actions) -->
552
  <button
553
  v-if="showUpload || showResults || showError"
 
877
  </button>
878
  </div>
879
 
880
+ <!-- Graphique 3D pour les images avec calibration réussie -->
881
+ <div v-if="uploadStore.isImage" class="result-chart">
882
+ <h3>Terrain de Football - Vue 3D</h3>
883
+ <PlotlyChart
884
+ data-type="football-field"
885
+ :height="550"
886
+ :keypoints-data="calibrationStore.results?.detections?.keypoints || []"
887
+ :camera-params="calibrationStore.results?.camera_parameters?.cam_params"
888
+ :custom-layout="{
889
+ title: {
890
+ text: 'Visualisation 3D du terrain de football',
891
+ font: { size: 16, color: 'white' }
892
+ },
893
+ paper_bgcolor: 'rgba(0,0,0,0)',
894
+ plot_bgcolor: 'rgba(0,0,0,0)'
895
+ }"
896
+ @data-loaded="onChartDataLoaded"
897
+ @error="onChartError"
898
+ />
899
+ </div>
900
+
901
  <details class="result-details">
902
  <summary>Données complètes</summary>
903
  <pre class="result-data">{{ JSON.stringify(calibrationStore.results, null, 2) }}</pre>
 
1494
 
1495
  /* Section Résultats */
1496
  .results-container {
1497
+ max-width: 800px;
1498
  width: 100%;
1499
  display: flex;
1500
  flex-direction: column;
 
1600
  }
1601
 
1602
  .result-data {
1603
+ background: #0d1117;
1604
+ color: #e6edf3;
1605
+ padding: 1.5rem;
1606
+ border-radius: 8px;
1607
+ font-family: 'Monaco', 'Consolas', 'Ubuntu Mono', monospace;
1608
+ font-size: 0.875rem;
1609
+ line-height: 1.6;
1610
  overflow-x: auto;
1611
+ white-space: pre;
1612
+ border: 1px solid #30363d;
1613
+ max-height: 400px;
 
1614
  overflow-y: auto;
1615
+ text-align: left;
1616
+ box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.3);
1617
+
1618
+ /* Syntax highlighting simulation */
1619
+ color: #c9d1d9;
1620
+ }
1621
+
1622
+ .result-details {
1623
+ position: relative;
1624
  }
1625
 
1626
  .confidence-score {
 
1855
  height: calc(100% - 80px);
1856
  }
1857
 
1858
+ /* Section graphique dans les résultats */
1859
+ .result-chart {
1860
+ width: 100%;
1861
+ max-width: 1000px;
1862
+ margin: 2rem 0;
1863
+ background: rgba(255, 255, 255, 0.02);
1864
+ border-radius: 12px;
1865
+ padding: 1.5rem;
1866
+ border: 1px solid rgba(255, 255, 255, 0.1);
1867
+ }
1868
+
1869
+ .result-chart h3 {
1870
+ font-size: 1.25rem;
1871
+ color: white;
1872
+ margin-bottom: 1rem;
1873
+ text-align: center;
1874
+ font-weight: 600;
1875
+ }
1876
+
1877
  /* Responsive */
1878
  @media (max-width: 768px) {
1879
  .home-container {
 
1900
  flex-direction: column;
1901
  }
1902
 
1903
+ .result-chart {
1904
+ padding: 1rem;
1905
+ margin: 1rem 0;
1906
+ }
1907
  }
1908
  </style>