saq1b commited on
Commit
b0f0c9f
·
verified ·
1 Parent(s): 430d0f8

Upload 2 files

Browse files
Files changed (2) hide show
  1. Dockerfile +18 -8
  2. index.html +443 -0
Dockerfile CHANGED
@@ -2,16 +2,26 @@ FROM python:3.9-slim
2
 
3
  WORKDIR /app
4
 
5
- COPY requirements.txt .
6
- RUN pip install --no-cache-dir -r requirements.txt
7
 
8
- COPY . .
 
9
 
10
- # Create directories if they don't exist
11
- RUN mkdir -p templates static
 
 
 
 
 
 
 
 
 
12
 
13
- # Expose the port that Flask will run on
14
  EXPOSE 7860
15
 
16
- # Command to run the Flask app
17
- CMD ["python", "app.py"]
 
2
 
3
  WORKDIR /app
4
 
5
+ # Install necessary packages
6
+ RUN pip install --no-cache-dir flask gunicorn
7
 
8
+ # Copy the HTML file
9
+ COPY index.html /app/static/index.html
10
 
11
+ # Create a simple Flask app to serve the static file
12
+ RUN echo 'from flask import Flask, redirect\n\
13
+ app = Flask(__name__, static_folder="static")\n\
14
+ \n\
15
+ @app.route("/")\n\
16
+ def index():\n\
17
+ return redirect("/static/index.html")\n\
18
+ \n\
19
+ if __name__ == "__main__":\n\
20
+ app.run(host="0.0.0.0", port=7860)\n\
21
+ ' > /app/app.py
22
 
23
+ # Expose the port that HuggingFace Spaces expects
24
  EXPOSE 7860
25
 
26
+ # Start the server
27
+ CMD ["gunicorn", "-b", "0.0.0.0:7860", "app:app"]
index.html ADDED
@@ -0,0 +1,443 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Trading Data Dashboard</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
10
+ </head>
11
+ <body class="bg-gray-100 min-h-screen">
12
+ <div class="container mx-auto px-4 py-8">
13
+ <!-- Header -->
14
+ <header class="mb-8">
15
+ <h1 class="text-4xl font-bold text-indigo-600 mb-2">Trading Data Analytics</h1>
16
+ <p class="text-gray-600">Explore trading patterns and item demand insights</p>
17
+ </header>
18
+
19
+ <!-- Stats Overview -->
20
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
21
+ <div class="bg-white p-6 rounded-lg shadow-md border-l-4 border-blue-500">
22
+ <h2 class="text-gray-500 text-sm font-medium">Total Items</h2>
23
+ <p class="text-3xl font-bold text-gray-800" id="total-items">Loading...</p>
24
+ </div>
25
+ <div class="bg-white p-6 rounded-lg shadow-md border-l-4 border-green-500">
26
+ <h2 class="text-gray-500 text-sm font-medium">Total Trades</h2>
27
+ <p class="text-3xl font-bold text-gray-800" id="total-trades">Loading...</p>
28
+ </div>
29
+ <div class="bg-white p-6 rounded-lg shadow-md border-l-4 border-yellow-500">
30
+ <h2 class="text-gray-500 text-sm font-medium">Avg. Demand Multiple</h2>
31
+ <p class="text-3xl font-bold text-gray-800" id="avg-demand">Loading...</p>
32
+ </div>
33
+ <div class="bg-white p-6 rounded-lg shadow-md border-l-4 border-purple-500">
34
+ <h2 class="text-gray-500 text-sm font-medium">Unique Item Types</h2>
35
+ <p class="text-3xl font-bold text-gray-800" id="unique-types">Loading...</p>
36
+ </div>
37
+ </div>
38
+
39
+ <!-- Controls -->
40
+ <div class="bg-white p-6 rounded-lg shadow-md mb-8">
41
+ <div class="flex flex-col md:flex-row md:items-center md:justify-between mb-4">
42
+ <div class="mb-4 md:mb-0">
43
+ <label class="block text-sm font-medium text-gray-700 mb-1">Search:</label>
44
+ <div class="relative">
45
+ <div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
46
+ <i class="fas fa-search text-gray-400"></i>
47
+ </div>
48
+ <input type="text" id="search-input" class="block w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:ring-indigo-500 focus:border-indigo-500" placeholder="Search by name...">
49
+ </div>
50
+ </div>
51
+
52
+ <div class="grid grid-cols-2 md:grid-cols-3 gap-4">
53
+ <div>
54
+ <label class="block text-sm font-medium text-gray-700 mb-1">Filter by Type:</label>
55
+ <select id="type-filter" class="block w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-indigo-500 focus:border-indigo-500">
56
+ <option value="all">All Types</option>
57
+ <!-- Will be populated dynamically -->
58
+ </select>
59
+ </div>
60
+ <div>
61
+ <label class="block text-sm font-medium text-gray-700 mb-1">Sort By:</label>
62
+ <select id="sort-by" class="block w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-indigo-500 focus:border-indigo-500">
63
+ <option value="Name">Name</option>
64
+ <option value="TimesTraded">Times Traded</option>
65
+ <option value="UniqueCirculation">Unique Circulation</option>
66
+ <option value="DemandMultiple" selected>Demand Multiple</option>
67
+ </select>
68
+ </div>
69
+ <div>
70
+ <label class="block text-sm font-medium text-gray-700 mb-1">Order:</label>
71
+ <select id="sort-order" class="block w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-indigo-500 focus:border-indigo-500">
72
+ <option value="desc" selected>Highest to Lowest</option>
73
+ <option value="asc">Lowest to Highest</option>
74
+ </select>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ </div>
79
+
80
+ <!-- Data Visualization -->
81
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
82
+ <div class="bg-white p-6 rounded-lg shadow-md">
83
+ <h2 class="text-xl font-semibold text-gray-800 mb-4">Top Items by Demand</h2>
84
+ <div class="h-80">
85
+ <canvas id="demand-chart"></canvas>
86
+ </div>
87
+ </div>
88
+ <div class="bg-white p-6 rounded-lg shadow-md">
89
+ <h2 class="text-xl font-semibold text-gray-800 mb-4">Distribution by Type</h2>
90
+ <div class="h-80">
91
+ <canvas id="type-chart"></canvas>
92
+ </div>
93
+ </div>
94
+ </div>
95
+
96
+ <!-- Data Table -->
97
+ <div class="bg-white rounded-lg shadow-md overflow-hidden">
98
+ <div class="px-6 py-4 border-b border-gray-200">
99
+ <h2 class="text-xl font-semibold text-gray-800">Trading Data</h2>
100
+ <p class="text-sm text-gray-500" id="results-count">Showing 0 items</p>
101
+ </div>
102
+ <div class="overflow-x-auto">
103
+ <table class="min-w-full divide-y divide-gray-200">
104
+ <thead class="bg-gray-50">
105
+ <tr>
106
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer" data-sort="Name">
107
+ Name <i class="fas fa-sort ml-1"></i>
108
+ </th>
109
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer" data-sort="Type">
110
+ Type <i class="fas fa-sort ml-1"></i>
111
+ </th>
112
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer" data-sort="TimesTraded">
113
+ Times Traded <i class="fas fa-sort ml-1"></i>
114
+ </th>
115
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer" data-sort="UniqueCirculation">
116
+ Circulation <i class="fas fa-sort ml-1"></i>
117
+ </th>
118
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer" data-sort="DemandMultiple">
119
+ Demand <i class="fas fa-sort ml-1"></i>
120
+ </th>
121
+ </tr>
122
+ </thead>
123
+ <tbody id="data-table-body" class="bg-white divide-y divide-gray-200">
124
+ <!-- Table rows will be inserted here -->
125
+ <tr>
126
+ <td colspan="5" class="px-6 py-4 text-center text-gray-500">Loading data...</td>
127
+ </tr>
128
+ </tbody>
129
+ </table>
130
+ </div>
131
+ </div>
132
+
133
+ <footer class="mt-8 text-center text-gray-600">
134
+ <p>Data refreshes periodically. Last updated: <span id="last-updated">Loading...</span></p>
135
+ </footer>
136
+ </div>
137
+
138
+ <script>
139
+ // API URL
140
+ const API_URL = 'https://badimo.nyc3.digitaloceanspaces.com/trade/frequency/snapshot/month/latest.json';
141
+
142
+ // Global data store
143
+ let tradeData = [];
144
+ let filteredData = [];
145
+
146
+ // Initialize the application
147
+ async function initialize() {
148
+ try {
149
+ const response = await fetch(API_URL);
150
+ if (!response.ok) {
151
+ throw new Error(`HTTP error! Status: ${response.status}`);
152
+ }
153
+
154
+ tradeData = await response.json();
155
+ filteredData = [...tradeData];
156
+
157
+ populateTypeFilter();
158
+ updateStats();
159
+ renderTable();
160
+ initCharts();
161
+ setupEventListeners();
162
+
163
+ document.getElementById('last-updated').textContent = new Date().toLocaleString();
164
+ } catch (error) {
165
+ console.error('Error fetching data:', error);
166
+ document.getElementById('data-table-body').innerHTML =
167
+ `<tr><td colspan="5" class="px-6 py-4 text-center text-red-500">
168
+ Error loading data. Please try again later.
169
+ </td></tr>`;
170
+ }
171
+ }
172
+
173
+ // Populate the type filter dropdown
174
+ function populateTypeFilter() {
175
+ const typeFilter = document.getElementById('type-filter');
176
+ const types = [...new Set(tradeData.map(item => item.Type))].sort();
177
+
178
+ types.forEach(type => {
179
+ const option = document.createElement('option');
180
+ option.value = type;
181
+ option.textContent = formatType(type);
182
+ typeFilter.appendChild(option);
183
+ });
184
+ }
185
+
186
+ // Format type for display
187
+ function formatType(type) {
188
+ if (type.includes('VehicleCustomization.')) {
189
+ return type.replace('VehicleCustomization.', '');
190
+ }
191
+ return type;
192
+ }
193
+
194
+ // Update statistics
195
+ function updateStats() {
196
+ const totalItems = filteredData.length;
197
+ const totalTrades = filteredData.reduce((sum, item) => sum + item.TimesTraded, 0);
198
+ const avgDemand = filteredData.reduce((sum, item) => sum + item.DemandMultiple, 0) / totalItems;
199
+ const uniqueTypes = new Set(filteredData.map(item => item.Type)).size;
200
+
201
+ document.getElementById('total-items').textContent = totalItems.toLocaleString();
202
+ document.getElementById('total-trades').textContent = totalTrades.toLocaleString();
203
+ document.getElementById('avg-demand').textContent = avgDemand.toFixed(2);
204
+ document.getElementById('unique-types').textContent = uniqueTypes;
205
+
206
+ document.getElementById('results-count').textContent = `Showing ${totalItems} items`;
207
+ }
208
+
209
+ // Render the data table
210
+ function renderTable() {
211
+ const tableBody = document.getElementById('data-table-body');
212
+ const sortBy = document.getElementById('sort-by').value;
213
+ const sortOrder = document.getElementById('sort-order').value;
214
+
215
+ // Sort data
216
+ filteredData.sort((a, b) => {
217
+ if (sortOrder === 'asc') {
218
+ return a[sortBy] > b[sortBy] ? 1 : -1;
219
+ } else {
220
+ return a[sortBy] < b[sortBy] ? 1 : -1;
221
+ }
222
+ });
223
+
224
+ // Generate table HTML
225
+ tableBody.innerHTML = filteredData.map(item => `
226
+ <tr class="hover:bg-gray-50">
227
+ <td class="px-6 py-4 whitespace-nowrap">
228
+ <div class="font-medium text-gray-900">${item.Name}</div>
229
+ </td>
230
+ <td class="px-6 py-4 whitespace-nowrap">
231
+ <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${getTypeColor(item.Type)}">
232
+ ${formatType(item.Type)}
233
+ </span>
234
+ </td>
235
+ <td class="px-6 py-4 whitespace-nowrap text-gray-800">
236
+ ${item.TimesTraded.toLocaleString()}
237
+ </td>
238
+ <td class="px-6 py-4 whitespace-nowrap text-gray-800">
239
+ ${item.UniqueCirculation.toLocaleString()}
240
+ </td>
241
+ <td class="px-6 py-4 whitespace-nowrap">
242
+ <div class="flex items-center">
243
+ <span class="mr-2 font-medium ${getDemandColor(item.DemandMultiple)}">
244
+ ${item.DemandMultiple.toFixed(2)}
245
+ </span>
246
+ <div class="w-16 bg-gray-200 rounded-full h-2.5">
247
+ <div class="${getDemandBarColor(item.DemandMultiple)} h-2.5 rounded-full" style="width: ${Math.min(item.DemandMultiple * 10, 100)}%"></div>
248
+ </div>
249
+ </div>
250
+ </td>
251
+ </tr>
252
+ `).join('');
253
+ }
254
+
255
+ // Get color class for type badge
256
+ function getTypeColor(type) {
257
+ if (type === 'Vehicle') return 'bg-blue-100 text-blue-800';
258
+ if (type.includes('Color')) return 'bg-purple-100 text-purple-800';
259
+ if (type.includes('Rim')) return 'bg-yellow-100 text-yellow-800';
260
+ if (type.includes('Texture')) return 'bg-green-100 text-green-800';
261
+ if (type.includes('Spoiler')) return 'bg-red-100 text-red-800';
262
+ return 'bg-gray-100 text-gray-800';
263
+ }
264
+
265
+ // Get color class for demand text
266
+ function getDemandColor(demand) {
267
+ if (demand >= 4) return 'text-red-600';
268
+ if (demand >= 3) return 'text-orange-600';
269
+ if (demand >= 2) return 'text-yellow-600';
270
+ return 'text-green-600';
271
+ }
272
+
273
+ // Get color class for demand progress bar
274
+ function getDemandBarColor(demand) {
275
+ if (demand >= 4) return 'bg-red-600';
276
+ if (demand >= 3) return 'bg-orange-500';
277
+ if (demand >= 2) return 'bg-yellow-500';
278
+ return 'bg-green-500';
279
+ }
280
+
281
+ // Initialize charts
282
+ function initCharts() {
283
+ renderDemandChart();
284
+ renderTypeChart();
285
+ }
286
+
287
+ // Render the demand chart
288
+ function renderDemandChart() {
289
+ const top10ByDemand = [...filteredData]
290
+ .sort((a, b) => b.DemandMultiple - a.DemandMultiple)
291
+ .slice(0, 10);
292
+
293
+ const ctx = document.getElementById('demand-chart').getContext('2d');
294
+
295
+ new Chart(ctx, {
296
+ type: 'bar',
297
+ data: {
298
+ labels: top10ByDemand.map(item => item.Name),
299
+ datasets: [{
300
+ label: 'Demand Multiple',
301
+ data: top10ByDemand.map(item => item.DemandMultiple),
302
+ backgroundColor: 'rgba(99, 102, 241, 0.6)',
303
+ borderColor: 'rgb(99, 102, 241)',
304
+ borderWidth: 1
305
+ }]
306
+ },
307
+ options: {
308
+ responsive: true,
309
+ maintainAspectRatio: false,
310
+ plugins: {
311
+ legend: {
312
+ display: false
313
+ }
314
+ },
315
+ scales: {
316
+ y: {
317
+ beginAtZero: true,
318
+ title: {
319
+ display: true,
320
+ text: 'Demand Multiple'
321
+ }
322
+ },
323
+ x: {
324
+ ticks: {
325
+ autoSkip: false,
326
+ maxRotation: 90,
327
+ minRotation: 45
328
+ }
329
+ }
330
+ }
331
+ }
332
+ });
333
+ }
334
+
335
+ // Render the type distribution chart
336
+ function renderTypeChart() {
337
+ const typeDistribution = filteredData.reduce((acc, item) => {
338
+ const type = formatType(item.Type);
339
+ if (!acc[type]) acc[type] = 0;
340
+ acc[type]++;
341
+ return acc;
342
+ }, {});
343
+
344
+ const types = Object.keys(typeDistribution);
345
+ const counts = Object.values(typeDistribution);
346
+
347
+ const colors = [
348
+ 'rgba(99, 102, 241, 0.6)', // Indigo
349
+ 'rgba(236, 72, 153, 0.6)', // Pink
350
+ 'rgba(245, 158, 11, 0.6)', // Amber
351
+ 'rgba(16, 185, 129, 0.6)', // Emerald
352
+ 'rgba(239, 68, 68, 0.6)', // Red
353
+ 'rgba(59, 130, 246, 0.6)', // Blue
354
+ 'rgba(139, 92, 246, 0.6)', // Violet
355
+ 'rgba(249, 115, 22, 0.6)', // Orange
356
+ 'rgba(107, 114, 128, 0.6)' // Gray
357
+ ];
358
+
359
+ const ctx = document.getElementById('type-chart').getContext('2d');
360
+
361
+ new Chart(ctx, {
362
+ type: 'doughnut',
363
+ data: {
364
+ labels: types,
365
+ datasets: [{
366
+ data: counts,
367
+ backgroundColor: colors.slice(0, types.length),
368
+ borderWidth: 1,
369
+ borderColor: '#fff'
370
+ }]
371
+ },
372
+ options: {
373
+ responsive: true,
374
+ maintainAspectRatio: false,
375
+ plugins: {
376
+ legend: {
377
+ position: 'right'
378
+ }
379
+ }
380
+ }
381
+ });
382
+ }
383
+
384
+ // Set up event listeners
385
+ function setupEventListeners() {
386
+ // Filter by type
387
+ document.getElementById('type-filter').addEventListener('change', applyFilters);
388
+
389
+ // Sort by column
390
+ document.getElementById('sort-by').addEventListener('change', renderTable);
391
+ document.getElementById('sort-order').addEventListener('change', renderTable);
392
+
393
+ // Search functionality
394
+ document.getElementById('search-input').addEventListener('input', applyFilters);
395
+
396
+ // Table header sorting
397
+ document.querySelectorAll('th[data-sort]').forEach(header => {
398
+ header.addEventListener('click', () => {
399
+ const sortField = header.getAttribute('data-sort');
400
+ const sortBySelect = document.getElementById('sort-by');
401
+ const sortOrderSelect = document.getElementById('sort-order');
402
+
403
+ if (sortBySelect.value === sortField) {
404
+ // Toggle order if already sorting by this field
405
+ sortOrderSelect.value = sortOrderSelect.value === 'asc' ? 'desc' : 'asc';
406
+ } else {
407
+ // Set new sort field and default to descending
408
+ sortBySelect.value = sortField;
409
+ sortOrderSelect.value = 'desc';
410
+ }
411
+
412
+ renderTable();
413
+ });
414
+ });
415
+ }
416
+
417
+ // Apply filters
418
+ function applyFilters() {
419
+ const typeFilter = document.getElementById('type-filter').value;
420
+ const searchTerm = document.getElementById('search-input').value.toLowerCase();
421
+
422
+ filteredData = tradeData.filter(item => {
423
+ const matchesType = typeFilter === 'all' || item.Type === typeFilter;
424
+ const matchesSearch = item.Name.toLowerCase().includes(searchTerm);
425
+ return matchesType && matchesSearch;
426
+ });
427
+
428
+ updateStats();
429
+ renderTable();
430
+
431
+ // Redraw charts with filtered data
432
+ const charts = Chart.instances;
433
+ for (let chart of charts) {
434
+ chart.destroy();
435
+ }
436
+ initCharts();
437
+ }
438
+
439
+ // Start the application
440
+ initialize();
441
+ </script>
442
+ </body>
443
+ </html>