MMOON commited on
Commit
bd5fd55
·
verified ·
1 Parent(s): 57f1429

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +104 -74
index.html CHANGED
@@ -23,14 +23,20 @@
23
  .guide-btn:hover { background-color: var(--primary-light); }
24
  .container { max-width: 1600px; margin: 2rem auto; padding: 0 2rem; }
25
 
 
 
 
 
 
 
 
26
  .controls-panel { background: var(--surface-color); border-radius: var(--border-radius); padding: 1.5rem; margin-bottom: 2rem; box-shadow: var(--shadow-md); border: 1px solid var(--border-color); }
27
  .view-selector { display: flex; flex-wrap: wrap; gap: 0.5rem; border-bottom: 1px solid var(--border-color); padding-bottom: 1rem; margin-bottom: 1rem; }
28
  .view-btn { padding: 0.6rem 1.2rem; border: 1px solid transparent; background: none; color: var(--text-secondary); cursor: pointer; font-size: 0.95rem; font-weight: 500; border-bottom: 3px solid transparent; border-radius: 6px; }
29
  .view-btn.active { color: var(--primary-light); background-color: #e7f3ff; border-color: var(--primary-light); }
30
 
31
- #main-view-controls { display: grid; grid-template-columns: 1fr auto auto; gap: 1rem; align-items: center; }
32
  #comparison-view-controls { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; align-items: center; }
33
-
34
  .search-container { position: relative; }
35
  .search-input { width: 100%; padding: 0.75rem 1rem 0.75rem 2.5rem; border: 1px solid var(--border-color); border-radius: var(--border-radius); font-size: 1rem; transition: var(--transition); }
36
  .search-input:focus { outline: none; border-color: var(--primary-light); box-shadow: 0 0 0 3px rgba(0, 85, 164, 0.1); }
@@ -53,7 +59,7 @@
53
  #takeaways-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 0.75rem; }
54
  #takeaways-list li { display: flex; align-items: flex-start; gap: 0.75rem; font-size: 0.9rem; }
55
  #takeaways-list li .icon { color: var(--warning-color); font-weight: bold; margin-top: 2px; }
56
-
57
  .table-container { background: var(--surface-color); border-radius: var(--border-radius); box-shadow: var(--shadow-md); border: 1px solid var(--border-color); overflow: hidden; }
58
  .table-header { padding: 1rem 1.5rem; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; }
59
  .table-title { font-weight: 600; margin: 0; }
@@ -75,7 +81,6 @@
75
  .score-high { background-color: #fdeded; color: #a50e0e; }
76
  .score-medium { background-color: #fff8e1; color: #6f4f12; }
77
  .score-low { background-color: #e6f4ea; color: #1b5e20; }
78
-
79
  #loading-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255, 255, 255, 0.9); z-index: 3000; display: flex; align-items: center; justify-content: center; text-align: center; font-size: 1.2rem; color: var(--primary-color); transition: opacity 0.3s; }
80
 
81
  .modal { display: none; position: fixed; z-index: 2000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.5); backdrop-filter: blur(5px); }
@@ -92,7 +97,7 @@
92
  .modal-footer a { color: var(--primary-light); }
93
 
94
  @keyframes fadeIn { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; transform: translateY(0); } }
95
- @media (max-width: 1200px) { #main-view-controls, #comparison-view-controls { grid-template-columns: 1fr; } .detail-layout { grid-template-columns: 1fr; } }
96
  @media (max-width: 768px) { .header-content, .container { padding: 0 1rem; } }
97
  </style>
98
  </head>
@@ -104,18 +109,16 @@
104
  <button class="guide-btn" id="guide-btn">📚 Guide d'Utilisation</button>
105
  </div>
106
  </header>
107
-
108
  <div class="container">
109
- <div id="welcome-guide" class="welcome-guide">
110
- <button id="close-guide" class="close-guide">×</button>
111
  <h3>Guide Rapide de l'Outil</h3>
112
  <div class="guide-steps">
113
- <div class="guide-step"><strong>1. Choisissez une Vue :</strong><br>Utilisez les boutons ci-dessous pour sélectionner votre niveau d'analyse.</div>
114
- <div class="guide-step"><strong>2. Filtrez les Données :</strong><br>Utilisez la recherche et les filtres pour affiner les résultats.</div>
115
- <div class="guide-step"><strong>3. Analysez en Profondeur :</strong><br>Cliquez sur les lignes du tableau ou utilisez la vue comparative.</div>
116
  </div>
117
  </div>
118
-
119
  <div class="controls-panel">
120
  <div class="view-selector">
121
  <button class="view-btn active" id="btn-ftl">Vue FTL</button>
@@ -123,14 +126,14 @@
123
  <button class="view-btn" id="btn-pairs">Analyse Détaillée</button>
124
  <button class="view-btn" id="btn-comparison">Analyse Comparative</button>
125
  </div>
126
- <div id="main-controls">
127
  <div class="controls-grid">
128
  <div class="search-container"><span class="search-icon">🔍</span><input type="search" class="search-input" id="searchInput" placeholder="Rechercher..."></div>
129
  <div id="hazard-filter-container" style="display: none;"><select class="filter-select" id="hazard-filter"><option value="">Tous les Dangers</option></select></div>
130
  </div>
131
  </div>
132
- <div id="comparison-controls" style="display: none;">
133
- <div class="controls-grid" style="grid-template-columns: 1fr 1fr;">
134
  <select class="filter-select" id="commodity-filter"></select>
135
  <select class="filter-select" id="comparison-hazard-filter"><option value="All">Tous les Dangers</option></select>
136
  </div>
@@ -144,7 +147,7 @@
144
  <div class="metric-card"><div class="metric-label">Catégories Uniques</div><div class="metric-value" id="categories-count">-</div></div>
145
  <div class="metric-card"><div class="metric-label">Score Moyen</div><div class="metric-value" id="avg-score">-</div></div>
146
  </div>
147
- <div id="detail-panel">
148
  <div class="detail-card"><div class="detail-layout">
149
  <div>
150
  <h3 class="chart-title" id="criteria-chart-title"></h3>
@@ -161,7 +164,8 @@
161
  <div class="table-wrapper"><table id="dataTable"><thead id="tableHead"></thead><tbody id="tableBody"></tbody></table></div>
162
  </div>
163
  </div>
164
- <div id="comparison-view-content">
 
165
  <div class="chart-container">
166
  <h3 class="chart-title" id="stacked-chart-title">Analyse des Critères Pondérés</h3>
167
  <div class="chart-canvas-container" id="comparison-chart-container"><canvas id="stackedBarChart"></canvas></div>
@@ -173,13 +177,13 @@
173
  <div class="modal-content">
174
  <div class="modal-header"><h2 class="modal-title">Guide d'Utilisation</h2><span class="close-modal" id="close-guide-btn">&times;</span></div>
175
  <h3>Comment utiliser cet outil</h3>
176
- <p>Cet outil est conçu pour vous aider à explorer le modèle de classement des risques de la FDA et à prioriser vos actions en sécurité alimentaire.</p>
177
  <h4>Les Vues d'Analyse</h4>
178
  <ul>
179
  <li><strong>Vue FTL :</strong> Se concentre sur les produits à plus haut risque de la Food Traceability List. Idéal pour les audits.</li>
180
  <li><strong>Vue Globale :</strong> Affiche tous les produits de la base de données pour une vue d'ensemble.</li>
181
  <li><strong>Analyse Détaillée :</strong> Montre les couples produit/danger. Cliquez sur une ligne pour une analyse approfondie (graphique radar et actions recommandées).</li>
182
- <li><strong>Analyse Comparative :</strong> Permet de comparer visuellement les contributions des facteurs de risque pour un produit et ses dangers.</li>
183
  </ul>
184
  <h3>Les 7 Critères d'Évaluation de la FDA</h3>
185
  <div class="criteria-grid">
@@ -194,38 +198,53 @@
194
  <div class="modal-footer">Source officielle : <a href="https://hfpappexternal.fda.gov/scripts/FDARiskRankingModelforFoodTracingfinalrule/" target="_blank" rel="noopener noreferrer">FDA Risk-Ranking Model</a>.</div>
195
  </div>
196
  </div>
197
-
198
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
199
  <script>
200
  document.addEventListener('DOMContentLoaded', function() {
201
  let state = {
202
  currentDataset: 'ftl', searchTerm: '', hazardFilter: '',
203
- sortColumn: 'agg_score', sortDirection: 'desc', charts: {},
204
- selectedCommodity: '', selectedHazard: 'All'
 
205
  };
206
  let fdaData = { aggregated_ftl: [], aggregated_all: [], pairs_all: [] };
207
 
208
  const viewConfig = {
209
- 'ftl': { dataKey: 'aggregated_ftl', title: 'Vue FTL', columns: [{ key: 'commodity', label: 'Produit', sortable: true }, { key: 'commodity_category', label: 'Catégorie', sortable: true }, { key: 'agg_score', label: 'Score', sortable: true, type: 'score' }], scoreColumn: 'agg_score' },
210
- 'all': { dataKey: 'aggregated_all', title: 'Vue Globale', columns: [{ key: 'commodity', label: 'Produit', sortable: true }, { key: 'commodity_category', label: 'Catégorie', sortable: true }, { key: 'agg_score', label: 'Score', sortable: true, type: 'score' }, { key: 'ftl', label: 'FTL', sortable: true, type: 'boolean' }], scoreColumn: 'agg_score' },
211
- 'pairs': { dataKey: 'pairs_all', title: 'Analyse Détaillée', columns: [{ key: 'commodity', label: 'Produit', sortable: true }, { key: 'hazardRef', label: 'Danger', sortable: true }, { key: 'risk_score', label: 'Score', sortable: true, type: 'score' }], scoreColumn: 'risk_score' },
212
  };
213
 
214
  const el = {
215
  loadingOverlay: document.getElementById('loading-overlay'),
216
- mainViewContent: document.getElementById('main-view-content'), comparisonViewContent: document.getElementById('comparison-view-content'),
217
- mainControls: document.getElementById('main-controls'), comparisonControls: document.getElementById('comparison-controls'),
218
- searchInput: document.getElementById('searchInput'), hazardFilter: document.getElementById('hazard-filter'), hazardFilterContainer: document.getElementById('hazard-filter-container'),
219
- commodityFilter: document.getElementById('commodity-filter'), comparisonHazardFilter: document.getElementById('comparison-hazard-filter'),
 
 
 
 
 
220
  buttons: { ftl: document.getElementById('btn-ftl'), all: document.getElementById('btn-all'), pairs: document.getElementById('btn-pairs'), comparison: document.getElementById('btn-comparison') },
221
  metrics: { totalProducts: document.getElementById('total-products'), highRiskCount: document.getElementById('high-risk-count'), categoriesCount: document.getElementById('categories-count'), avgScore: document.getElementById('avg-score')},
222
  detailPanel: document.getElementById('detail-panel'),
223
- criteriaChartTitle: document.getElementById('criteria-chart-title'), criteriaCanvas: document.getElementById('criteriaChart'),
224
- takeawaysSection: document.getElementById('takeaways-section'), takeawaysList: document.getElementById('takeaways-list'),
225
- tableTitle: document.getElementById('table-title'), tableHead: document.getElementById('tableHead'), tableBody: document.getElementById('tableBody'), resultsCount: document.getElementById('results-count'),
226
- stackedBarChartCanvas: document.getElementById('stackedBarChart'), stackedChartTitle: document.getElementById('stacked-chart-title'),
227
- welcomeGuide: document.getElementById('welcome-guide'), closeGuide: document.getElementById('close-guide'),
228
- guideModal: document.getElementById('guide-modal'), guideBtn: document.getElementById('guide-btn'), closeGuideBtn: document.getElementById('close-guide-btn')
 
 
 
 
 
 
 
 
 
229
  };
230
 
231
  async function loadAllData() {
@@ -236,9 +255,8 @@
236
  };
237
  try {
238
  const responses = await Promise.all(Object.values(files).map(file => fetch(file)));
239
- for (const res of responses) { if (!res.ok) throw new Error(`Échec du chargement du fichier : ${res.url}`); }
240
  const [ftl, all, pairs] = await Promise.all(responses.map(res => res.json()));
241
-
242
  const clean = item => ({ ...item, commodity: String(item.commodity).replace(/<ast>\*<\/ast>/g, '').trim() });
243
  fdaData.aggregated_ftl = ftl.map(clean);
244
  fdaData.aggregated_all = all.map(clean);
@@ -263,9 +281,13 @@
263
  el.comparisonControls.style.display = isComparisonView ? 'block' : 'none';
264
 
265
  Object.values(el.buttons).forEach(btn => btn.classList.remove('active'));
266
- el.buttons[state.currentDataset].classList.add('active');
267
 
268
- if (isComparisonView) renderComparisonView(); else renderMainView();
 
 
 
 
269
  }
270
 
271
  function renderMainView() {
@@ -277,10 +299,51 @@
277
  document.querySelectorAll('#tableHead th').forEach(th => { th.classList.remove('sort-asc', 'sort-desc'); if (th.dataset.column === state.sortColumn) th.classList.add(`sort-${state.sortDirection}`); });
278
  el.tableBody.innerHTML = data.length === 0 ? `<tr><td colspan="${config.columns.length}" style="text-align:center;">🚫 Aucun résultat</td></tr>` : data.map((row, index) => `<tr class="${state.currentDataset === 'pairs' ? 'clickable' : ''}" data-index="${index}">${config.columns.map(col => `<td>${formatCell(row[col.key], col.type)}</td>`).join('')}</tr>`).join('');
279
  updateDashboard(data);
280
- el.hazardFilterContainer.style.display = state.currentDataset === 'pairs' ? 'block' : 'none';
281
  if (state.currentDataset !== 'pairs') el.detailPanel.classList.remove('visible');
282
  }
283
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
  function updateDashboard(data) {
285
  const scoreCol = viewConfig[state.currentDataset].scoreColumn;
286
  const scores = data.map(i => i[scoreCol]).filter(s => typeof s === 'number');
@@ -289,16 +352,14 @@
289
  el.metrics.categoriesCount.textContent = new Set(data.map(i => i.commodity_category).filter(Boolean)).size;
290
  el.metrics.avgScore.textContent = scores.length ? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length) : 'N/A';
291
  }
292
-
293
  function showDetailPanel(rowIndex) {
294
  const data = filterAndSortData()[rowIndex];
295
  if (!data || !('C1' in data)) return;
296
-
297
  el.detailPanel.classList.add('visible');
298
  el.criteriaCanvas.style.display = 'block';
299
  el.takeawaysSection.style.display = 'block';
300
  el.criteriaChartTitle.textContent = `Analyse de "${data.commodity}"`;
301
-
302
  if (state.charts.criteria) state.charts.criteria.destroy();
303
  state.charts.criteria = new Chart(el.criteriaCanvas, {
304
  type: 'radar',
@@ -320,37 +381,6 @@
320
  el.detailPanel.scrollIntoView({ behavior: 'smooth', block: 'start' });
321
  }
322
 
323
- function renderComparisonView() {
324
- let data = fdaData.pairs_all.filter(p => p.commodity === state.selectedCommodity);
325
- if (state.selectedHazard !== 'All') data = data.filter(p => p.hazardRef === state.selectedHazard);
326
- el.stackedChartTitle.textContent = `Analyse des Critères Pondérés pour : ${state.selectedCommodity}`;
327
-
328
- if (state.charts.stackedBar) state.charts.stackedBar.destroy();
329
- const criteria = ['C1w', 'C2w', 'C3w', 'C4w', 'C5w', 'C6w', 'C7w'];
330
- const labels = ['C1: Éclosions', 'C2: Gravité', 'C3: Contamination', 'C4: Croissance', 'C5: Intervention', 'C6: Consommation', 'C7: Coût'];
331
- const colors = ['#e57373', '#d69f70', '#81c784', '#4dd0e1', '#64b5f6', '#7986cb', '#ba68c8'];
332
-
333
- state.charts.stackedBar = new Chart(el.stackedBarChartCanvas, {
334
- type: 'bar',
335
- data: { labels: data.map(d => d.hazardRef), datasets: criteria.map((c, i) => ({ label: labels[i], data: data.map(d => d[c]), backgroundColor: colors[i] }))},
336
- options: { indexAxis: 'y', responsive: true, maintainAspectRatio: false, scales: { x: { stacked: true, title: { display: true, text: 'Score Pondéré' } }, y: { stacked: true } }, plugins: { legend: { position: 'top' } } }
337
- });
338
- }
339
-
340
- function populateFilters() {
341
- const mainHazards = [...new Set(fdaData.pairs_all.map(item => item.hazardRef))].filter(Boolean).sort();
342
- el.hazardFilter.innerHTML = '<option value="">Tous les Dangers</option>' + mainHazards.map(h => `<option value="${h}">${h}</option>`).join('');
343
-
344
- const commodities = [...new Set(fdaData.pairs_all.map(item => item.commodity))].sort();
345
- el.commodityFilter.innerHTML = commodities.map(c => `<option value="${c}">${c}</option>`).join('');
346
- if (commodities.length > 0) { state.selectedCommodity = commodities[0]; el.commodityFilter.value = state.selectedCommodity; populateComparisonHazardFilter(); }
347
- }
348
-
349
- function populateComparisonHazardFilter(){
350
- const hazards = [...new Set(fdaData.pairs_all.filter(p => p.commodity === state.selectedCommodity).map(p => p.hazardRef))].sort();
351
- el.comparisonHazardFilter.innerHTML = '<option value="All">Tous les Dangers</option>' + hazards.map(h => `<option value="${h}">${h}</option>`).join('');
352
- }
353
-
354
  function setupEventListeners() {
355
  el.searchInput.addEventListener('input', () => { state.searchTerm = el.searchInput.value; render(); });
356
  el.hazardFilter.addEventListener('change', () => { state.hazardFilter = el.hazardFilter.value; render(); });
 
23
  .guide-btn:hover { background-color: var(--primary-light); }
24
  .container { max-width: 1600px; margin: 2rem auto; padding: 0 2rem; }
25
 
26
+ .welcome-guide { background-color: #e7f3ff; border: 1px solid #a8c7fa; border-radius: var(--border-radius); padding: 1.5rem; margin-bottom: 2rem; position: relative; animation: fadeIn 0.5s; }
27
+ .welcome-guide h3 { margin: 0 0 1rem 0; color: var(--primary-color); }
28
+ .guide-steps { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1.5rem; }
29
+ .guide-step { font-size: 0.9rem; }
30
+ .guide-step strong { color: var(--text-primary); }
31
+ .close-guide { position: absolute; top: 0.5rem; right: 0.5rem; background: none; border: none; font-size: 1.5rem; cursor: pointer; color: var(--text-secondary); }
32
+
33
  .controls-panel { background: var(--surface-color); border-radius: var(--border-radius); padding: 1.5rem; margin-bottom: 2rem; box-shadow: var(--shadow-md); border: 1px solid var(--border-color); }
34
  .view-selector { display: flex; flex-wrap: wrap; gap: 0.5rem; border-bottom: 1px solid var(--border-color); padding-bottom: 1rem; margin-bottom: 1rem; }
35
  .view-btn { padding: 0.6rem 1.2rem; border: 1px solid transparent; background: none; color: var(--text-secondary); cursor: pointer; font-size: 0.95rem; font-weight: 500; border-bottom: 3px solid transparent; border-radius: 6px; }
36
  .view-btn.active { color: var(--primary-light); background-color: #e7f3ff; border-color: var(--primary-light); }
37
 
38
+ #main-view-controls { display: grid; grid-template-columns: 1fr auto; gap: 1rem; align-items: center; }
39
  #comparison-view-controls { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; align-items: center; }
 
40
  .search-container { position: relative; }
41
  .search-input { width: 100%; padding: 0.75rem 1rem 0.75rem 2.5rem; border: 1px solid var(--border-color); border-radius: var(--border-radius); font-size: 1rem; transition: var(--transition); }
42
  .search-input:focus { outline: none; border-color: var(--primary-light); box-shadow: 0 0 0 3px rgba(0, 85, 164, 0.1); }
 
59
  #takeaways-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 0.75rem; }
60
  #takeaways-list li { display: flex; align-items: flex-start; gap: 0.75rem; font-size: 0.9rem; }
61
  #takeaways-list li .icon { color: var(--warning-color); font-weight: bold; margin-top: 2px; }
62
+
63
  .table-container { background: var(--surface-color); border-radius: var(--border-radius); box-shadow: var(--shadow-md); border: 1px solid var(--border-color); overflow: hidden; }
64
  .table-header { padding: 1rem 1.5rem; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; }
65
  .table-title { font-weight: 600; margin: 0; }
 
81
  .score-high { background-color: #fdeded; color: #a50e0e; }
82
  .score-medium { background-color: #fff8e1; color: #6f4f12; }
83
  .score-low { background-color: #e6f4ea; color: #1b5e20; }
 
84
  #loading-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255, 255, 255, 0.9); z-index: 3000; display: flex; align-items: center; justify-content: center; text-align: center; font-size: 1.2rem; color: var(--primary-color); transition: opacity 0.3s; }
85
 
86
  .modal { display: none; position: fixed; z-index: 2000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.5); backdrop-filter: blur(5px); }
 
97
  .modal-footer a { color: var(--primary-light); }
98
 
99
  @keyframes fadeIn { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; transform: translateY(0); } }
100
+ @media (max-width: 992px) { #main-view-controls, #comparison-view-controls { grid-template-columns: 1fr; } .detail-layout { grid-template-columns: 1fr; } }
101
  @media (max-width: 768px) { .header-content, .container { padding: 0 1rem; } }
102
  </style>
103
  </head>
 
109
  <button class="guide-btn" id="guide-btn">📚 Guide d'Utilisation</button>
110
  </div>
111
  </header>
 
112
  <div class="container">
113
+ <div class="welcome-guide" id="welcome-guide">
114
+ <button class="close-guide" id="close-guide">×</button>
115
  <h3>Guide Rapide de l'Outil</h3>
116
  <div class="guide-steps">
117
+ <div class="guide-step"><strong>1. Choisissez une Vue :</strong><br>Utilisez les boutons ci-dessous pour sélectionner votre niveau d'analyse.</div>
118
+ <div class="guide-step"><strong>2. Filtrez les Données :</strong><br>Utilisez la recherche et les filtres pour affiner les résultats.</div>
119
+ <div class="guide-step"><strong>3. Analysez en Profondeur :</strong><br>Cliquez sur les lignes du tableau ou utilisez la vue comparative.</div>
120
  </div>
121
  </div>
 
122
  <div class="controls-panel">
123
  <div class="view-selector">
124
  <button class="view-btn active" id="btn-ftl">Vue FTL</button>
 
126
  <button class="view-btn" id="btn-pairs">Analyse Détaillée</button>
127
  <button class="view-btn" id="btn-comparison">Analyse Comparative</button>
128
  </div>
129
+ <div id="main-view-controls">
130
  <div class="controls-grid">
131
  <div class="search-container"><span class="search-icon">🔍</span><input type="search" class="search-input" id="searchInput" placeholder="Rechercher..."></div>
132
  <div id="hazard-filter-container" style="display: none;"><select class="filter-select" id="hazard-filter"><option value="">Tous les Dangers</option></select></div>
133
  </div>
134
  </div>
135
+ <div id="comparison-view-controls" style="display: none;">
136
+ <div class="controls-grid" style="grid-template-columns: 1fr 1fr;">
137
  <select class="filter-select" id="commodity-filter"></select>
138
  <select class="filter-select" id="comparison-hazard-filter"><option value="All">Tous les Dangers</option></select>
139
  </div>
 
147
  <div class="metric-card"><div class="metric-label">Catégories Uniques</div><div class="metric-value" id="categories-count">-</div></div>
148
  <div class="metric-card"><div class="metric-label">Score Moyen</div><div class="metric-value" id="avg-score">-</div></div>
149
  </div>
150
+ <div id="detail-panel">
151
  <div class="detail-card"><div class="detail-layout">
152
  <div>
153
  <h3 class="chart-title" id="criteria-chart-title"></h3>
 
164
  <div class="table-wrapper"><table id="dataTable"><thead id="tableHead"></thead><tbody id="tableBody"></tbody></table></div>
165
  </div>
166
  </div>
167
+
168
+ <div id="comparison-view-content" style="display: none;">
169
  <div class="chart-container">
170
  <h3 class="chart-title" id="stacked-chart-title">Analyse des Critères Pondérés</h3>
171
  <div class="chart-canvas-container" id="comparison-chart-container"><canvas id="stackedBarChart"></canvas></div>
 
177
  <div class="modal-content">
178
  <div class="modal-header"><h2 class="modal-title">Guide d'Utilisation</h2><span class="close-modal" id="close-guide-btn">&times;</span></div>
179
  <h3>Comment utiliser cet outil</h3>
180
+ <p>Cet outil vous aide à explorer le modèle de classement des risques de la FDA pour prioriser vos efforts en sécurité alimentaire.</p>
181
  <h4>Les Vues d'Analyse</h4>
182
  <ul>
183
  <li><strong>Vue FTL :</strong> Se concentre sur les produits à plus haut risque de la Food Traceability List. Idéal pour les audits.</li>
184
  <li><strong>Vue Globale :</strong> Affiche tous les produits de la base de données pour une vue d'ensemble.</li>
185
  <li><strong>Analyse Détaillée :</strong> Montre les couples produit/danger. Cliquez sur une ligne pour une analyse approfondie (graphique radar et actions recommandées).</li>
186
+ <li><strong>Analyse Comparative :</strong> Permet de comparer visuellement les contributions des différents facteurs de risque pour un produit et ses dangers.</li>
187
  </ul>
188
  <h3>Les 7 Critères d'Évaluation de la FDA</h3>
189
  <div class="criteria-grid">
 
198
  <div class="modal-footer">Source officielle : <a href="https://hfpappexternal.fda.gov/scripts/FDARiskRankingModelforFoodTracingfinalrule/" target="_blank" rel="noopener noreferrer">FDA Risk-Ranking Model</a>.</div>
199
  </div>
200
  </div>
201
+
202
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
203
  <script>
204
  document.addEventListener('DOMContentLoaded', function() {
205
  let state = {
206
  currentDataset: 'ftl', searchTerm: '', hazardFilter: '',
207
+ sortColumn: 'agg_score', sortDirection: 'desc',
208
+ selectedCommodity: '', selectedHazard: 'All',
209
+ charts: {}
210
  };
211
  let fdaData = { aggregated_ftl: [], aggregated_all: [], pairs_all: [] };
212
 
213
  const viewConfig = {
214
+ 'ftl': { dataKey: 'aggregated_ftl', title: 'Vue FTL (Produits à Traçabilité Renforcée)', columns: [{ key: 'commodity', label: 'Produit', sortable: true }, { key: 'commodity_category', label: 'Catégorie', sortable: true }, { key: 'agg_score', label: 'Score', sortable: true, type: 'score' }], scoreColumn: 'agg_score' },
215
+ 'all': { dataKey: 'aggregated_all', title: 'Vue Globale (Tous les Produits)', columns: [{ key: 'commodity', label: 'Produit', sortable: true }, { key: 'commodity_category', label: 'Catégorie', sortable: true }, { key: 'agg_score', label: 'Score', sortable: true, type: 'score' }, { key: 'ftl', label: 'FTL', sortable: true, type: 'boolean' }], scoreColumn: 'agg_score' },
216
+ 'pairs': { dataKey: 'pairs_all', title: 'Analyse Détaillée (Produit x Danger)', columns: [{ key: 'commodity', label: 'Produit', sortable: true }, { key: 'hazardRef', label: 'Danger', sortable: true }, { key: 'risk_score', label: 'Score', sortable: true, type: 'score' }], scoreColumn: 'risk_score' },
217
  };
218
 
219
  const el = {
220
  loadingOverlay: document.getElementById('loading-overlay'),
221
+ mainViewContent: document.getElementById('main-view-content'),
222
+ comparisonViewContent: document.getElementById('comparison-view-content'),
223
+ mainControls: document.getElementById('main-controls'),
224
+ comparisonControls: document.getElementById('comparison-controls'),
225
+ searchInput: document.getElementById('searchInput'),
226
+ hazardFilter: document.getElementById('hazard-filter'),
227
+ hazardFilterContainer: document.getElementById('hazard-filter-container'),
228
+ commodityFilter: document.getElementById('commodity-filter'),
229
+ comparisonHazardFilter: document.getElementById('comparison-hazard-filter'),
230
  buttons: { ftl: document.getElementById('btn-ftl'), all: document.getElementById('btn-all'), pairs: document.getElementById('btn-pairs'), comparison: document.getElementById('btn-comparison') },
231
  metrics: { totalProducts: document.getElementById('total-products'), highRiskCount: document.getElementById('high-risk-count'), categoriesCount: document.getElementById('categories-count'), avgScore: document.getElementById('avg-score')},
232
  detailPanel: document.getElementById('detail-panel'),
233
+ criteriaChartTitle: document.getElementById('criteria-chart-title'),
234
+ criteriaCanvas: document.getElementById('criteriaChart'),
235
+ takeawaysSection: document.getElementById('takeaways-section'),
236
+ takeawaysList: document.getElementById('takeaways-list'),
237
+ tableTitle: document.getElementById('table-title'),
238
+ tableHead: document.getElementById('tableHead'),
239
+ tableBody: document.getElementById('tableBody'),
240
+ resultsCount: document.getElementById('results-count'),
241
+ stackedBarChartCanvas: document.getElementById('stackedBarChart'),
242
+ stackedChartTitle: document.getElementById('stacked-chart-title'),
243
+ welcomeGuide: document.getElementById('welcome-guide'),
244
+ closeGuide: document.getElementById('close-guide'),
245
+ guideModal: document.getElementById('guide-modal'),
246
+ guideBtn: document.getElementById('guide-btn'),
247
+ closeGuideBtn: document.getElementById('close-guide-btn')
248
  };
249
 
250
  async function loadAllData() {
 
255
  };
256
  try {
257
  const responses = await Promise.all(Object.values(files).map(file => fetch(file)));
258
+ for (const res of responses) { if (!res.ok) throw new Error(`Échec du chargement : ${res.url}`); }
259
  const [ftl, all, pairs] = await Promise.all(responses.map(res => res.json()));
 
260
  const clean = item => ({ ...item, commodity: String(item.commodity).replace(/<ast>\*<\/ast>/g, '').trim() });
261
  fdaData.aggregated_ftl = ftl.map(clean);
262
  fdaData.aggregated_all = all.map(clean);
 
281
  el.comparisonControls.style.display = isComparisonView ? 'block' : 'none';
282
 
283
  Object.values(el.buttons).forEach(btn => btn.classList.remove('active'));
284
+ if(el.buttons[state.currentDataset]) el.buttons[state.currentDataset].classList.add('active');
285
 
286
+ if (isComparisonView) {
287
+ renderComparisonView();
288
+ } else {
289
+ renderMainView();
290
+ }
291
  }
292
 
293
  function renderMainView() {
 
299
  document.querySelectorAll('#tableHead th').forEach(th => { th.classList.remove('sort-asc', 'sort-desc'); if (th.dataset.column === state.sortColumn) th.classList.add(`sort-${state.sortDirection}`); });
300
  el.tableBody.innerHTML = data.length === 0 ? `<tr><td colspan="${config.columns.length}" style="text-align:center;">🚫 Aucun résultat</td></tr>` : data.map((row, index) => `<tr class="${state.currentDataset === 'pairs' ? 'clickable' : ''}" data-index="${index}">${config.columns.map(col => `<td>${formatCell(row[col.key], col.type)}</td>`).join('')}</tr>`).join('');
301
  updateDashboard(data);
302
+ el.hazardFilterContainer.style.display = state.currentDataset === 'pairs' ? 'flex' : 'none';
303
  if (state.currentDataset !== 'pairs') el.detailPanel.classList.remove('visible');
304
  }
305
 
306
+ function renderComparisonView() {
307
+ let data = fdaData.pairs_all.filter(p => p.commodity === state.selectedCommodity);
308
+ if (state.selectedHazard !== 'All') data = data.filter(p => p.hazardRef === state.selectedHazard);
309
+ el.stackedChartTitle.textContent = `Analyse des Critères Pondérés pour : ${state.selectedCommodity}`;
310
+
311
+ if (state.charts.stackedBar) state.charts.stackedBar.destroy();
312
+ const criteria = ['C1w', 'C2w', 'C3w', 'C4w', 'C5w', 'C6w', 'C7w'];
313
+ const labels = ['C1: Éclosions', 'C2: Gravité', 'C3: Contamination', 'C4: Croissance', 'C5: Intervention', 'C6: Consommation', 'C7: Coût'];
314
+ const colors = ['#e57373', '#d69f70', '#81c784', '#4dd0e1', '#64b5f6', '#7986cb', '#ba68c8'];
315
+
316
+ state.charts.stackedBar = new Chart(el.stackedBarChartCanvas, {
317
+ type: 'bar',
318
+ data: { labels: data.map(d => d.hazardRef), datasets: criteria.map((c, i) => ({ label: labels[i], data: data.map(d => d[c]), backgroundColor: colors[i] }))},
319
+ options: { indexAxis: 'y', responsive: true, maintainAspectRatio: false, scales: { x: { stacked: true, title: { display: true, text: 'Score Pondéré' } }, y: { stacked: true } }, plugins: { legend: { position: 'top' } } }
320
+ });
321
+ }
322
+
323
+ function filterAndSortData() {
324
+ const config = viewConfig[state.currentDataset]; let data = [...fdaData[config.dataKey]];
325
+ if (state.searchTerm) { const search = state.searchTerm.toLowerCase(); data = data.filter(row => Object.values(row).some(v => String(v).toLowerCase().includes(search))); }
326
+ if (state.currentDataset === 'pairs' && state.hazardFilter) { data = data.filter(row => row.hazardRef === state.hazardFilter); }
327
+ if (state.sortColumn) {
328
+ const isNumeric = config.columns.find(c => c.key === state.sortColumn)?.type === 'score';
329
+ data.sort((a, b) => { const valA = a[state.sortColumn], valB = b[state.sortColumn]; const comparison = isNumeric ? (valA || 0) - (valB || 0) : String(valA || '').localeCompare(String(valB || '')); return state.sortDirection === 'asc' ? comparison : -comparison; });
330
+ }
331
+ return data;
332
+ }
333
+
334
+ function populateFilters() {
335
+ const mainHazards = [...new Set(fdaData.pairs_all.map(item => item.hazardRef))].filter(Boolean).sort();
336
+ el.hazardFilter.innerHTML = '<option value="">Tous les Dangers</option>' + mainHazards.map(h => `<option value="${h}">${h}</option>`).join('');
337
+ const commodities = [...new Set(fdaData.pairs_all.map(item => item.commodity))].sort();
338
+ el.commodityFilter.innerHTML = commodities.map(c => `<option value="${c}">${c}</option>`).join('');
339
+ if (commodities.length > 0) { state.selectedCommodity = commodities[0]; el.commodityFilter.value = state.selectedCommodity; populateComparisonHazardFilter(); }
340
+ }
341
+
342
+ function populateComparisonHazardFilter(){
343
+ const hazards = [...new Set(fdaData.pairs_all.filter(p => p.commodity === state.selectedCommodity).map(p => p.hazardRef))].sort();
344
+ el.comparisonHazardFilter.innerHTML = '<option value="All">Tous les Dangers</option>' + hazards.map(h => `<option value="${h}">${h}</option>`).join('');
345
+ }
346
+
347
  function updateDashboard(data) {
348
  const scoreCol = viewConfig[state.currentDataset].scoreColumn;
349
  const scores = data.map(i => i[scoreCol]).filter(s => typeof s === 'number');
 
352
  el.metrics.categoriesCount.textContent = new Set(data.map(i => i.commodity_category).filter(Boolean)).size;
353
  el.metrics.avgScore.textContent = scores.length ? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length) : 'N/A';
354
  }
355
+
356
  function showDetailPanel(rowIndex) {
357
  const data = filterAndSortData()[rowIndex];
358
  if (!data || !('C1' in data)) return;
 
359
  el.detailPanel.classList.add('visible');
360
  el.criteriaCanvas.style.display = 'block';
361
  el.takeawaysSection.style.display = 'block';
362
  el.criteriaChartTitle.textContent = `Analyse de "${data.commodity}"`;
 
363
  if (state.charts.criteria) state.charts.criteria.destroy();
364
  state.charts.criteria = new Chart(el.criteriaCanvas, {
365
  type: 'radar',
 
381
  el.detailPanel.scrollIntoView({ behavior: 'smooth', block: 'start' });
382
  }
383
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
  function setupEventListeners() {
385
  el.searchInput.addEventListener('input', () => { state.searchTerm = el.searchInput.value; render(); });
386
  el.hazardFilter.addEventListener('change', () => { state.hazardFilter = el.hazardFilter.value; render(); });