|
1 | 1 | <script lang="ts" setup>
|
| 2 | +import { ref, computed } from 'vue' |
| 3 | +import { useHead } from '@vueuse/head' |
| 4 | +import { Line, Bar } from 'vue-chartjs' |
| 5 | +import { |
| 6 | + Chart as ChartJS, |
| 7 | + CategoryScale, |
| 8 | + LinearScale, |
| 9 | + PointElement, |
| 10 | + LineElement, |
| 11 | + BarElement, |
| 12 | + Title, |
| 13 | + Tooltip, |
| 14 | + Legend, |
| 15 | + Filler, |
| 16 | + Scale, |
| 17 | + ChartOptions, |
| 18 | + ScaleOptionsByType, |
| 19 | + CartesianScaleTypeRegistry, |
| 20 | + CoreScaleOptions, |
| 21 | + Tick, |
| 22 | +} from 'chart.js' |
| 23 | +
|
| 24 | +ChartJS.register( |
| 25 | + CategoryScale, |
| 26 | + LinearScale, |
| 27 | + PointElement, |
| 28 | + LineElement, |
| 29 | + BarElement, |
| 30 | + Title, |
| 31 | + Tooltip, |
| 32 | + Legend, |
| 33 | + Filler, |
| 34 | +) |
| 35 | +
|
2 | 36 | useHead({
|
3 | 37 | title: 'Dashboard - Packages',
|
4 | 38 | })
|
@@ -163,6 +197,237 @@ const packages = [
|
163 | 197 | ]
|
164 | 198 |
|
165 | 199 | const getPackageUrl = (pkgName: string) => `https://github.com/stacksjs/${pkgName}`
|
| 200 | +
|
| 201 | +const colors = [ |
| 202 | + { border: 'rgb(59, 130, 246)', background: 'rgba(59, 130, 246, 0.8)' }, |
| 203 | + { border: 'rgb(234, 179, 8)', background: 'rgba(234, 179, 8, 0.8)' }, |
| 204 | + { border: 'rgb(239, 68, 68)', background: 'rgba(239, 68, 68, 0.8)' }, |
| 205 | + { border: 'rgb(16, 185, 129)', background: 'rgba(16, 185, 129, 0.8)' }, |
| 206 | + { border: 'rgb(168, 85, 247)', background: 'rgba(168, 85, 247, 0.8)' }, |
| 207 | +] |
| 208 | +
|
| 209 | +// Update the chart options to use proper typing |
| 210 | +const baseChartOptions = { |
| 211 | + responsive: true, |
| 212 | + maintainAspectRatio: false, |
| 213 | + scales: { |
| 214 | + y: { |
| 215 | + type: 'linear' as const, |
| 216 | + beginAtZero: true, |
| 217 | + min: 0, |
| 218 | + grid: { |
| 219 | + color: 'rgba(200, 200, 200, 0.1)', |
| 220 | + }, |
| 221 | + ticks: { |
| 222 | + color: 'rgb(156, 163, 175)', |
| 223 | + font: { |
| 224 | + family: "'JetBrains Mono', monospace", |
| 225 | + }, |
| 226 | + callback: function(this: Scale<CoreScaleOptions>, tickValue: number | string, index: number, ticks: Tick[]) { |
| 227 | + const value = Number(tickValue) |
| 228 | + if (value >= 1000000) return `${(value / 1000000).toFixed(1)}M` |
| 229 | + if (value >= 1000) return `${(value / 1000).toFixed(1)}k` |
| 230 | + return value.toString() |
| 231 | + } |
| 232 | + }, |
| 233 | + }, |
| 234 | + x: { |
| 235 | + type: 'category' as const, |
| 236 | + grid: { |
| 237 | + display: false, |
| 238 | + }, |
| 239 | + ticks: { |
| 240 | + color: 'rgb(156, 163, 175)', |
| 241 | + font: { |
| 242 | + family: "'JetBrains Mono', monospace", |
| 243 | + }, |
| 244 | + }, |
| 245 | + }, |
| 246 | + }, |
| 247 | + plugins: { |
| 248 | + legend: { |
| 249 | + display: true, |
| 250 | + position: 'top' as const, |
| 251 | + align: 'end' as const, |
| 252 | + labels: { |
| 253 | + color: 'rgb(156, 163, 175)', |
| 254 | + font: { |
| 255 | + family: "'JetBrains Mono', monospace", |
| 256 | + }, |
| 257 | + boxWidth: 12, |
| 258 | + padding: 15, |
| 259 | + }, |
| 260 | + }, |
| 261 | + }, |
| 262 | +} |
| 263 | +
|
| 264 | +const timeChartOptions = { |
| 265 | + ...baseChartOptions, |
| 266 | + interaction: { |
| 267 | + mode: 'index' as const, |
| 268 | + intersect: false, |
| 269 | + }, |
| 270 | + plugins: { |
| 271 | + ...baseChartOptions.plugins, |
| 272 | + tooltip: { |
| 273 | + mode: 'index' as const, |
| 274 | + intersect: false, |
| 275 | + callbacks: { |
| 276 | + label: (context: any) => { |
| 277 | + let label = context.dataset.label || '' |
| 278 | + if (label) label += ': ' |
| 279 | + if (context.parsed.y !== null) { |
| 280 | + const value = context.parsed.y |
| 281 | + if (value >= 1000000) return `${label}${(value / 1000000).toFixed(1)}M downloads` |
| 282 | + if (value >= 1000) return `${label}${(value / 1000).toFixed(1)}k downloads` |
| 283 | + return `${label}${value} downloads` |
| 284 | + } |
| 285 | + return label |
| 286 | + } |
| 287 | + } |
| 288 | + } |
| 289 | + }, |
| 290 | +} |
| 291 | +
|
| 292 | +// Sort packages by downloads for better visualization |
| 293 | +const sortedPackages = computed(() => { |
| 294 | + return [...packages].sort((a, b) => b.downloads - a.downloads) |
| 295 | +}) |
| 296 | +
|
| 297 | +// Chart data for downloads |
| 298 | +const downloadsData = computed(() => ({ |
| 299 | + labels: sortedPackages.value.map(pkg => pkg.name), |
| 300 | + datasets: [{ |
| 301 | + label: 'Downloads', |
| 302 | + data: sortedPackages.value.map(pkg => pkg.downloads), |
| 303 | + backgroundColor: 'rgba(59, 130, 246, 0.8)', |
| 304 | + borderColor: 'rgb(59, 130, 246)', |
| 305 | + borderWidth: 1 |
| 306 | + }] |
| 307 | +})) |
| 308 | +
|
| 309 | +// Chart data for contributors |
| 310 | +const contributorsData = computed(() => ({ |
| 311 | + labels: sortedPackages.value.map(pkg => pkg.name), |
| 312 | + datasets: [{ |
| 313 | + label: 'Contributors', |
| 314 | + data: sortedPackages.value.map(pkg => pkg.contributors), |
| 315 | + backgroundColor: 'rgba(234, 179, 8, 0.8)', |
| 316 | + borderColor: 'rgb(234, 179, 8)', |
| 317 | + borderWidth: 1 |
| 318 | + }] |
| 319 | +})) |
| 320 | +
|
| 321 | +// Chart data for issues |
| 322 | +const issuesData = computed(() => ({ |
| 323 | + labels: sortedPackages.value.map(pkg => pkg.name), |
| 324 | + datasets: [{ |
| 325 | + label: 'Open Issues', |
| 326 | + data: sortedPackages.value.map(pkg => pkg.issues), |
| 327 | + backgroundColor: 'rgba(239, 68, 68, 0.8)', |
| 328 | + borderColor: 'rgb(239, 68, 68)', |
| 329 | + borderWidth: 1 |
| 330 | + }] |
| 331 | +})) |
| 332 | +
|
| 333 | +// Customize options for different metrics |
| 334 | +const contributorsOptions = { |
| 335 | + ...baseChartOptions, |
| 336 | + plugins: { |
| 337 | + ...baseChartOptions.plugins, |
| 338 | + tooltip: { |
| 339 | + callbacks: { |
| 340 | + label: (context: any) => { |
| 341 | + return `Contributors: ${context.parsed.y}` |
| 342 | + } |
| 343 | + } |
| 344 | + } |
| 345 | + } |
| 346 | +} as const |
| 347 | +
|
| 348 | +const issuesOptions = { |
| 349 | + ...baseChartOptions, |
| 350 | + plugins: { |
| 351 | + ...baseChartOptions.plugins, |
| 352 | + tooltip: { |
| 353 | + callbacks: { |
| 354 | + label: (context: any) => { |
| 355 | + return `Open Issues: ${context.parsed.y}` |
| 356 | + } |
| 357 | + } |
| 358 | + } |
| 359 | + } |
| 360 | +} as const |
| 361 | +
|
| 362 | +const timeRange = ref<'7' | '30' | '90' | '365'>('30') |
| 363 | +const displayMode = ref<'line' | 'bar'>('line') |
| 364 | +const selectedPackages = ref<Set<string>>(new Set()) |
| 365 | +const isLoading = ref(false) |
| 366 | +
|
| 367 | +// Helper function to format dates |
| 368 | +const formatDate = (daysAgo: number) => { |
| 369 | + const date = new Date() |
| 370 | + date.setDate(date.getDate() - daysAgo) |
| 371 | + return date.toLocaleDateString('en-US', { |
| 372 | + month: 'short', |
| 373 | + day: 'numeric' |
| 374 | + }) |
| 375 | +} |
| 376 | +
|
| 377 | +// Generate date labels for the selected time range |
| 378 | +const generateDateLabels = (days: number) => { |
| 379 | + return Array.from({ length: days }, (_, i) => formatDate(days - 1 - i)).reverse() |
| 380 | +} |
| 381 | +
|
| 382 | +// Generate mock daily download data for a package |
| 383 | +const generateDailyData = (baseDownloads: number, days: number) => { |
| 384 | + return Array.from({ length: days }, () => { |
| 385 | + const variance = baseDownloads * 0.2 // 20% variance |
| 386 | + return Math.floor(baseDownloads / days + (Math.random() - 0.5) * variance) |
| 387 | + }) |
| 388 | +} |
| 389 | +
|
| 390 | +// Computed property to determine if we should use compact mode |
| 391 | +const useCompactMode = computed(() => packages.length > 10) |
| 392 | +
|
| 393 | +// Computed property for filtered and sorted packages |
| 394 | +const filteredPackages = computed(() => { |
| 395 | + let filtered = [...packages] |
| 396 | + if (useCompactMode.value && selectedPackages.value.size === 0) { |
| 397 | + // In compact mode, default to showing top 5 packages by downloads |
| 398 | + filtered = filtered.sort((a, b) => b.downloads - a.downloads).slice(0, 5) |
| 399 | + } else if (selectedPackages.value.size > 0) { |
| 400 | + filtered = filtered.filter(pkg => selectedPackages.value.has(pkg.name)) |
| 401 | + } |
| 402 | + return filtered.sort((a, b) => b.downloads - a.downloads) |
| 403 | +}) |
| 404 | +
|
| 405 | +// Chart data for downloads over time |
| 406 | +const downloadsTimeData = computed(() => { |
| 407 | + const days = parseInt(timeRange.value) |
| 408 | + const labels = generateDateLabels(days) |
| 409 | +
|
| 410 | + return { |
| 411 | + labels, |
| 412 | + datasets: filteredPackages.value.map((pkg, index) => { |
| 413 | + const colorIndex = index % colors.length |
| 414 | + const color = colors[colorIndex] |
| 415 | + if (!color) return null |
| 416 | +
|
| 417 | + return { |
| 418 | + label: pkg.name, |
| 419 | + data: generateDailyData(pkg.downloads, days), |
| 420 | + borderColor: color.border, |
| 421 | + backgroundColor: displayMode.value === 'line' |
| 422 | + ? color.background.replace('0.8', '0.1') |
| 423 | + : color.background, |
| 424 | + fill: true, |
| 425 | + tension: 0.4 |
| 426 | + } |
| 427 | + }).filter((dataset): dataset is NonNullable<typeof dataset> => dataset !== null) |
| 428 | + } |
| 429 | +}) |
| 430 | +
|
166 | 431 | </script>
|
167 | 432 |
|
168 | 433 | <template>
|
@@ -229,6 +494,111 @@ const getPackageUrl = (pkgName: string) => `https://github.com/stacksjs/${pkgNam
|
229 | 494 | </div>
|
230 | 495 | </div>
|
231 | 496 |
|
| 497 | + <!-- Charts Section --> |
| 498 | + <div class="mb-8 px-4 lg:px-8 sm:px-6"> |
| 499 | + <div class="grid grid-cols-1 gap-8"> |
| 500 | + <!-- Downloads Chart --> |
| 501 | + <div class="bg-white dark:bg-blue-gray-700 rounded-lg shadow"> |
| 502 | + <div class="p-6"> |
| 503 | + <div class="flex items-center justify-between mb-6"> |
| 504 | + <div> |
| 505 | + <h3 class="text-base font-medium text-gray-900 dark:text-gray-100">Package Downloads</h3> |
| 506 | + <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Downloads over time per package</p> |
| 507 | + </div> |
| 508 | + <div class="flex items-center space-x-4"> |
| 509 | + <select |
| 510 | + v-model="timeRange" |
| 511 | + class="h-9 text-sm border-0 rounded-md bg-gray-50 dark:bg-blue-gray-600 py-1.5 pl-3 pr-8 text-gray-900 dark:text-gray-100 ring-1 ring-inset ring-gray-300 dark:ring-gray-600 focus:ring-2 focus:ring-blue-600" |
| 512 | + > |
| 513 | + <option value="7">Last 7 days</option> |
| 514 | + <option value="30">Last 30 days</option> |
| 515 | + <option value="90">Last 90 days</option> |
| 516 | + <option value="365">Last year</option> |
| 517 | + </select> |
| 518 | + <div v-if="useCompactMode" class="flex items-center space-x-2"> |
| 519 | + <select |
| 520 | + v-model="selectedPackages" |
| 521 | + multiple |
| 522 | + class="h-9 text-sm border-0 rounded-md bg-gray-50 dark:bg-blue-gray-600 py-1.5 pl-3 pr-8 text-gray-900 dark:text-gray-100 ring-1 ring-inset ring-gray-300 dark:ring-gray-600 focus:ring-2 focus:ring-blue-600 max-h-32" |
| 523 | + > |
| 524 | + <option v-for="pkg in packages" :key="pkg.name" :value="pkg.name"> |
| 525 | + {{ pkg.name }} |
| 526 | + </option> |
| 527 | + </select> |
| 528 | + <button |
| 529 | + @click="selectedPackages.clear()" |
| 530 | + class="h-9 px-3 text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 ring-1 ring-inset ring-gray-300 dark:ring-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-blue-gray-500" |
| 531 | + > |
| 532 | + Clear |
| 533 | + </button> |
| 534 | + </div> |
| 535 | + <div class="flex rounded-md shadow-sm h-9"> |
| 536 | + <button |
| 537 | + @click="displayMode = 'line'" |
| 538 | + :class="[ |
| 539 | + 'px-3 py-2 text-sm font-semibold rounded-l-md ring-1 ring-inset transition-colors', |
| 540 | + displayMode === 'line' |
| 541 | + ? 'bg-blue-600 text-white ring-blue-600' |
| 542 | + : 'bg-white text-gray-600 hover:bg-gray-50 ring-gray-300 dark:bg-blue-gray-600 dark:text-gray-200 dark:ring-gray-600 dark:hover:bg-blue-gray-500' |
| 543 | + ]" |
| 544 | + > |
| 545 | + Line |
| 546 | + </button> |
| 547 | + <button |
| 548 | + @click="displayMode = 'bar'" |
| 549 | + :class="[ |
| 550 | + 'px-3 py-2 text-sm font-semibold rounded-r-md ring-1 ring-inset transition-colors -ml-px', |
| 551 | + displayMode === 'bar' |
| 552 | + ? 'bg-blue-600 text-white ring-blue-600' |
| 553 | + : 'bg-white text-gray-600 hover:bg-gray-50 ring-gray-300 dark:bg-blue-gray-600 dark:text-gray-200 dark:ring-gray-600 dark:hover:bg-blue-gray-500' |
| 554 | + ]" |
| 555 | + > |
| 556 | + Bar |
| 557 | + </button> |
| 558 | + </div> |
| 559 | + </div> |
| 560 | + </div> |
| 561 | + <div class="h-[400px] relative"> |
| 562 | + <div v-if="isLoading" class="absolute inset-0 flex items-center justify-center bg-white bg-opacity-75 dark:bg-blue-gray-700 dark:bg-opacity-75 z-10"> |
| 563 | + <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div> |
| 564 | + </div> |
| 565 | + <component |
| 566 | + :is="displayMode === 'line' ? Line : Bar" |
| 567 | + :data="downloadsTimeData" |
| 568 | + :options="timeChartOptions" |
| 569 | + /> |
| 570 | + </div> |
| 571 | + </div> |
| 572 | + </div> |
| 573 | + |
| 574 | + <!-- Contributors Chart --> |
| 575 | + <div class="bg-white dark:bg-blue-gray-700 rounded-lg shadow"> |
| 576 | + <div class="p-6"> |
| 577 | + <div class="mb-6"> |
| 578 | + <h3 class="text-base font-medium text-gray-900 dark:text-gray-100">Package Contributors</h3> |
| 579 | + <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Number of contributors per package</p> |
| 580 | + </div> |
| 581 | + <div class="h-[400px]"> |
| 582 | + <Bar :data="contributorsData" :options="contributorsOptions" /> |
| 583 | + </div> |
| 584 | + </div> |
| 585 | + </div> |
| 586 | + |
| 587 | + <!-- Issues Chart --> |
| 588 | + <div class="bg-white dark:bg-blue-gray-700 rounded-lg shadow"> |
| 589 | + <div class="p-6"> |
| 590 | + <div class="mb-6"> |
| 591 | + <h3 class="text-base font-medium text-gray-900 dark:text-gray-100">Open Issues</h3> |
| 592 | + <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Number of open issues per package</p> |
| 593 | + </div> |
| 594 | + <div class="h-[400px]"> |
| 595 | + <Bar :data="issuesData" :options="issuesOptions" /> |
| 596 | + </div> |
| 597 | + </div> |
| 598 | + </div> |
| 599 | + </div> |
| 600 | + </div> |
| 601 | + |
232 | 602 | <!-- Packages Table section -->
|
233 | 603 | <div class="px-4 pt-12 lg:px-8 sm:px-6">
|
234 | 604 | <div class="sm:flex sm:items-center">
|
|
0 commit comments