Xianbao QIAN
commited on
Commit
·
c88f96b
1
Parent(s):
31ec36d
allow download csv
Browse files- src/pages/trend/index.tsx +191 -158
- src/utils/modelData.ts +35 -0
src/pages/trend/index.tsx
CHANGED
@@ -16,7 +16,8 @@ import {
|
|
16 |
getTotalMonthlyData,
|
17 |
processDetailedModelData,
|
18 |
MonthlyActivity,
|
19 |
-
DetailedModelData
|
|
|
20 |
} from '../../utils/modelData';
|
21 |
|
22 |
interface TrendProps {
|
@@ -161,180 +162,212 @@ const TrendPage: React.FC<TrendProps> = ({ monthlyData = [], totalData = [], det
|
|
161 |
Track the growth of Chinese AI models and datasets over time
|
162 |
</p>
|
163 |
|
164 |
-
<div className="flex
|
165 |
-
<
|
166 |
-
onClick={() => setContentType('all')}
|
167 |
-
className={`px-4 py-2 rounded-lg ${
|
168 |
-
contentType === 'all'
|
169 |
-
? 'bg-blue-500 text-white'
|
170 |
-
: 'bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-300'
|
171 |
-
}`}
|
172 |
-
>
|
173 |
-
All
|
174 |
-
</button>
|
175 |
-
<button
|
176 |
-
onClick={() => setContentType('models')}
|
177 |
-
className={`px-4 py-2 rounded-lg ${
|
178 |
-
contentType === 'models'
|
179 |
-
? 'bg-blue-500 text-white'
|
180 |
-
: 'bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-300'
|
181 |
-
}`}
|
182 |
-
>
|
183 |
-
Models
|
184 |
-
</button>
|
185 |
-
<button
|
186 |
-
onClick={() => setContentType('datasets')}
|
187 |
-
className={`px-4 py-2 rounded-lg ${
|
188 |
-
contentType === 'datasets'
|
189 |
-
? 'bg-blue-500 text-white'
|
190 |
-
: 'bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-300'
|
191 |
-
}`}
|
192 |
-
>
|
193 |
-
Datasets
|
194 |
-
</button>
|
195 |
-
</div>
|
196 |
-
|
197 |
-
{/* Controls */}
|
198 |
-
<div className="flex flex-wrap gap-4 mb-6 justify-center items-center">
|
199 |
-
<div className="flex gap-2">
|
200 |
<button
|
201 |
-
onClick={() =>
|
202 |
-
className={`px-4 py-2 rounded
|
203 |
-
|
204 |
-
? 'bg-
|
205 |
-
: 'bg-gray-200 dark:bg-gray-700 dark:text-gray-300'
|
206 |
}`}
|
207 |
>
|
208 |
-
|
209 |
</button>
|
210 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
211 |
<button
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
: 'text-gray-600 dark:text-gray-400'
|
218 |
}`}
|
219 |
-
style={{
|
220 |
-
backgroundColor: selectedProviders.includes(provider)
|
221 |
-
? color
|
222 |
-
: 'transparent'
|
223 |
-
}}
|
224 |
>
|
225 |
-
{
|
226 |
</button>
|
227 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
228 |
</div>
|
229 |
-
</div>
|
230 |
|
231 |
-
|
232 |
-
|
233 |
-
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
|
241 |
-
|
242 |
-
|
243 |
-
|
244 |
-
|
245 |
-
/>
|
246 |
-
<YAxis stroke="#9CA3AF" />
|
247 |
-
<Tooltip content={<CustomTooltip />} />
|
248 |
-
<Legend />
|
249 |
-
|
250 |
-
{/* Total Line */}
|
251 |
-
{showTotal && (
|
252 |
-
<Line
|
253 |
-
data={filteredTotalData}
|
254 |
-
type="monotone"
|
255 |
-
dataKey="count"
|
256 |
-
stroke={COLORS['Total']}
|
257 |
-
name="Total"
|
258 |
-
strokeWidth={2}
|
259 |
-
dot={false}
|
260 |
/>
|
261 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
262 |
|
263 |
-
|
264 |
-
|
265 |
-
|
266 |
-
|
267 |
-
|
268 |
-
|
269 |
-
|
270 |
-
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
|
275 |
-
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
|
280 |
-
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
|
285 |
-
|
286 |
-
|
287 |
-
|
288 |
-
|
289 |
-
|
290 |
-
|
291 |
-
|
292 |
-
|
293 |
-
|
294 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
295 |
</div>
|
296 |
-
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
|
303 |
-
|
304 |
-
|
305 |
-
<
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
326 |
-
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
331 |
</div>
|
332 |
</div>
|
333 |
-
|
334 |
-
|
335 |
</div>
|
336 |
-
|
337 |
-
|
338 |
</div>
|
339 |
</div>
|
340 |
</div>
|
|
|
16 |
getTotalMonthlyData,
|
17 |
processDetailedModelData,
|
18 |
MonthlyActivity,
|
19 |
+
DetailedModelData,
|
20 |
+
convertToCSV
|
21 |
} from '../../utils/modelData';
|
22 |
|
23 |
interface TrendProps {
|
|
|
162 |
Track the growth of Chinese AI models and datasets over time
|
163 |
</p>
|
164 |
|
165 |
+
<div className="flex flex-col gap-4 p-4">
|
166 |
+
<div className="flex justify-center mb-6 space-x-4">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
167 |
<button
|
168 |
+
onClick={() => setContentType('all')}
|
169 |
+
className={`px-4 py-2 rounded-lg ${
|
170 |
+
contentType === 'all'
|
171 |
+
? 'bg-blue-500 text-white'
|
172 |
+
: 'bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-300'
|
173 |
}`}
|
174 |
>
|
175 |
+
All
|
176 |
</button>
|
177 |
+
<button
|
178 |
+
onClick={() => setContentType('models')}
|
179 |
+
className={`px-4 py-2 rounded-lg ${
|
180 |
+
contentType === 'models'
|
181 |
+
? 'bg-blue-500 text-white'
|
182 |
+
: 'bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-300'
|
183 |
+
}`}
|
184 |
+
>
|
185 |
+
Models
|
186 |
+
</button>
|
187 |
+
<button
|
188 |
+
onClick={() => setContentType('datasets')}
|
189 |
+
className={`px-4 py-2 rounded-lg ${
|
190 |
+
contentType === 'datasets'
|
191 |
+
? 'bg-blue-500 text-white'
|
192 |
+
: 'bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-300'
|
193 |
+
}`}
|
194 |
+
>
|
195 |
+
Datasets
|
196 |
+
</button>
|
197 |
+
</div>
|
198 |
+
|
199 |
+
<div className="flex flex-wrap gap-4 mb-6 justify-center items-center">
|
200 |
+
<div className="flex gap-2">
|
201 |
<button
|
202 |
+
onClick={() => setShowTotal(!showTotal)}
|
203 |
+
className={`px-4 py-2 rounded-lg ${
|
204 |
+
showTotal
|
205 |
+
? 'bg-blue-500 text-white'
|
206 |
+
: 'bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-300'
|
|
|
207 |
}`}
|
|
|
|
|
|
|
|
|
|
|
208 |
>
|
209 |
+
{showTotal ? 'Hide Total' : 'Show Total'}
|
210 |
</button>
|
211 |
+
{Object.entries(PROVIDERS_MAP).map(([provider, { color }]) => (
|
212 |
+
<button
|
213 |
+
key={provider}
|
214 |
+
onClick={() => toggleProvider(provider)}
|
215 |
+
className={`px-4 py-2 rounded-lg transition-colors ${
|
216 |
+
selectedProviders.includes(provider)
|
217 |
+
? 'text-white'
|
218 |
+
: 'text-gray-600 dark:text-gray-400'
|
219 |
+
}`}
|
220 |
+
style={{
|
221 |
+
backgroundColor: selectedProviders.includes(provider)
|
222 |
+
? color
|
223 |
+
: 'transparent'
|
224 |
+
}}
|
225 |
+
>
|
226 |
+
{provider}
|
227 |
+
</button>
|
228 |
+
))}
|
229 |
+
</div>
|
230 |
</div>
|
|
|
231 |
|
232 |
+
{/* Chart */}
|
233 |
+
<div className="w-full h-[600px] dark:bg-gray-900 p-4 rounded-lg">
|
234 |
+
<ResponsiveContainer width="100%" height="100%">
|
235 |
+
<LineChart
|
236 |
+
margin={{ top: 20, right: 30, left: 20, bottom: 20 }}
|
237 |
+
>
|
238 |
+
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
|
239 |
+
<XAxis
|
240 |
+
dataKey="date"
|
241 |
+
type="category"
|
242 |
+
allowDuplicatedCategory={false}
|
243 |
+
tick={{ fontSize: 12 }}
|
244 |
+
interval="preserveStartEnd"
|
245 |
+
stroke="#9CA3AF"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
246 |
/>
|
247 |
+
<YAxis stroke="#9CA3AF" />
|
248 |
+
<Tooltip content={<CustomTooltip />} />
|
249 |
+
<Legend />
|
250 |
+
|
251 |
+
{/* Total Line */}
|
252 |
+
{showTotal && (
|
253 |
+
<Line
|
254 |
+
data={filteredTotalData}
|
255 |
+
type="monotone"
|
256 |
+
dataKey="count"
|
257 |
+
stroke={COLORS['Total']}
|
258 |
+
name="Total"
|
259 |
+
strokeWidth={2}
|
260 |
+
dot={false}
|
261 |
+
/>
|
262 |
+
)}
|
263 |
|
264 |
+
{/* Provider Lines */}
|
265 |
+
{selectedProviders.map(provider => (
|
266 |
+
<Line
|
267 |
+
key={provider}
|
268 |
+
data={providerData[provider]}
|
269 |
+
type="monotone"
|
270 |
+
dataKey="count"
|
271 |
+
stroke={COLORS[provider]}
|
272 |
+
name={provider}
|
273 |
+
strokeWidth={1.5}
|
274 |
+
dot={false}
|
275 |
+
/>
|
276 |
+
))}
|
277 |
+
</LineChart>
|
278 |
+
</ResponsiveContainer>
|
279 |
+
</div>
|
280 |
|
281 |
+
{/* Download Button */}
|
282 |
+
<div className="flex justify-center mt-4 mb-8">
|
283 |
+
<button
|
284 |
+
className="px-4 py-2 rounded-lg bg-green-500 text-white hover:bg-green-600"
|
285 |
+
onClick={() => {
|
286 |
+
// Get the currently visible data based on filters
|
287 |
+
const visibleData = [];
|
288 |
+
|
289 |
+
// Add provider data if showing
|
290 |
+
selectedProviders.forEach(provider => {
|
291 |
+
visibleData.push(...providerData[provider].map(d => ({ ...d, provider })));
|
292 |
+
});
|
293 |
+
|
294 |
+
// Add total data if showing
|
295 |
+
if (showTotal) {
|
296 |
+
visibleData.push(...filteredTotalData);
|
297 |
+
}
|
298 |
+
|
299 |
+
const csvContent = convertToCSV(visibleData);
|
300 |
+
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
301 |
+
const link = document.createElement('a');
|
302 |
+
link.href = URL.createObjectURL(blob);
|
303 |
+
link.download = `${contentType}_counts.csv`;
|
304 |
+
link.click();
|
305 |
+
URL.revokeObjectURL(link.href);
|
306 |
+
}}
|
307 |
+
>
|
308 |
+
Download CSV
|
309 |
+
</button>
|
310 |
</div>
|
311 |
+
|
312 |
+
{/* Major Releases Section */}
|
313 |
+
<div className="mt-12">
|
314 |
+
<div className="flex items-center justify-between mb-6">
|
315 |
+
<h2 className="text-2xl font-bold dark:text-white">
|
316 |
+
Major Releases ({Object.values(filteredModels).flat().length})
|
317 |
+
</h2>
|
318 |
+
<div className="flex items-center gap-3 bg-white dark:bg-gray-800 px-4 py-2 rounded-lg shadow-sm">
|
319 |
+
<label className="text-sm font-medium dark:text-gray-300">Min Likes:</label>
|
320 |
+
<input
|
321 |
+
type="number"
|
322 |
+
value={minLikes}
|
323 |
+
onChange={(e) => setMinLikes(Math.max(0, parseInt(e.target.value) || 0))}
|
324 |
+
className="w-20 px-2 py-1 border rounded dark:bg-gray-700 dark:border-gray-600 dark:text-white focus:outline-none focus:ring-2 focus:ring-green-500"
|
325 |
+
/>
|
326 |
+
</div>
|
327 |
+
</div>
|
328 |
+
<div className="space-y-8">
|
329 |
+
{Object.entries(filteredModels)
|
330 |
+
.map(([monthKey, models]) => ({
|
331 |
+
monthKey,
|
332 |
+
models,
|
333 |
+
sortKey: models[0]?.sortKey || '' // Use the first model's sortKey
|
334 |
+
}))
|
335 |
+
.sort((a, b) => b.sortKey.localeCompare(a.sortKey)) // Sort by sortKey in descending order
|
336 |
+
.map(({ monthKey, models }) => (
|
337 |
+
<div key={monthKey} className="border-b border-gray-200 dark:border-gray-700 pb-4">
|
338 |
+
<h3 className="text-xl font-semibold mb-3 dark:text-white">{monthKey}</h3>
|
339 |
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
340 |
+
{models
|
341 |
+
.sort((a, b) => b.likes - a.likes) // Sort models within each month by likes
|
342 |
+
.map(model => (
|
343 |
+
<div
|
344 |
+
key={model.id}
|
345 |
+
className="p-4 rounded-lg border dark:border-gray-700 hover:shadow-md transition-shadow bg-white dark:bg-gray-800"
|
346 |
+
style={{ borderColor: COLORS[model.provider] }}
|
347 |
+
>
|
348 |
+
<div className="flex justify-between items-start">
|
349 |
+
<div>
|
350 |
+
<a
|
351 |
+
href={`https://huggingface.co/${model.id}`}
|
352 |
+
target="_blank"
|
353 |
+
rel="noopener noreferrer"
|
354 |
+
className="text-blue-600 hover:underline font-medium dark:text-blue-400"
|
355 |
+
>
|
356 |
+
{model.name}
|
357 |
+
</a>
|
358 |
+
<p className="text-sm text-gray-600 dark:text-gray-400">{model.provider}</p>
|
359 |
+
</div>
|
360 |
+
<div className="text-right">
|
361 |
+
<p className="text-sm font-medium dark:text-white">❤️ {model.likes}</p>
|
362 |
+
<p className="text-xs text-gray-500 dark:text-gray-400">{formatDate(model.createdAt)}</p>
|
363 |
+
</div>
|
364 |
</div>
|
365 |
</div>
|
366 |
+
))}
|
367 |
+
</div>
|
368 |
</div>
|
369 |
+
))}
|
370 |
+
</div>
|
371 |
</div>
|
372 |
</div>
|
373 |
</div>
|
src/utils/modelData.ts
CHANGED
@@ -318,3 +318,38 @@ export const getTotalMonthlyData = (monthlyData: MonthlyActivity[]): MonthlyActi
|
|
318 |
}
|
319 |
]).sort((a, b) => a.date.localeCompare(b.date));
|
320 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
318 |
}
|
319 |
]).sort((a, b) => a.date.localeCompare(b.date));
|
320 |
};
|
321 |
+
|
322 |
+
// Convert monthly activity data to CSV format
|
323 |
+
export const convertToCSV = (data: MonthlyActivity[]): string => {
|
324 |
+
// Group data by date
|
325 |
+
const dataByDate: Record<string, Record<string, number>> = {};
|
326 |
+
const providers = new Set<string>();
|
327 |
+
|
328 |
+
// Initialize and collect data
|
329 |
+
data.forEach(({ date, provider, count }) => {
|
330 |
+
if (!dataByDate[date]) {
|
331 |
+
dataByDate[date] = {};
|
332 |
+
}
|
333 |
+
dataByDate[date][provider] = count;
|
334 |
+
providers.add(provider);
|
335 |
+
});
|
336 |
+
|
337 |
+
// Create CSV header
|
338 |
+
const header = ['Date', ...Array.from(providers)];
|
339 |
+
|
340 |
+
// Create CSV rows
|
341 |
+
const rows = Object.entries(dataByDate)
|
342 |
+
.sort(([a], [b]) => a.localeCompare(b))
|
343 |
+
.map(([date, providerData]) => {
|
344 |
+
const row = [date];
|
345 |
+
header.slice(1).forEach(provider => {
|
346 |
+
row.push((providerData[provider] || 0).toString());
|
347 |
+
});
|
348 |
+
return row;
|
349 |
+
});
|
350 |
+
|
351 |
+
// Combine header and rows
|
352 |
+
return [header, ...rows]
|
353 |
+
.map(row => row.join(','))
|
354 |
+
.join('\n');
|
355 |
+
};
|