|
| 1 | +<template> |
| 2 | + <div class="space-y-4"> |
| 3 | + <div v-if="loading" class="h-64 w-full animate-pulse rounded-lg bg-slate-800/40" /> |
| 4 | + <p |
| 5 | + v-else-if="error" |
| 6 | + class="rounded-lg border border-red-500/40 bg-red-900/30 p-4 text-sm text-red-200" |
| 7 | + > |
| 8 | + {{ error }} |
| 9 | + </p> |
| 10 | + <p |
| 11 | + v-else-if="!buckets.length" |
| 12 | + class="rounded-lg border border-slate-700 bg-slate-900/40 p-4 text-center text-sm text-slate-300" |
| 13 | + > |
| 14 | + {{ emptyMessage }} |
| 15 | + </p> |
| 16 | + <Line v-else :data="chartData" :options="chartOptions" class="h-64 w-full" /> |
| 17 | + </div> |
| 18 | +</template> |
| 19 | + |
| 20 | +<script setup lang="ts"> |
| 21 | +import { computed } from 'vue'; |
| 22 | +import { Line } from 'vue-chartjs'; |
| 23 | +import { |
| 24 | + Chart as ChartJS, |
| 25 | + CategoryScale, |
| 26 | + LinearScale, |
| 27 | + PointElement, |
| 28 | + LineElement, |
| 29 | + Tooltip, |
| 30 | + Legend, |
| 31 | +} from 'chart.js'; |
| 32 | +
|
| 33 | +ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Tooltip, Legend); |
| 34 | +
|
| 35 | +type Bucket = { |
| 36 | + bucket_start: number; |
| 37 | + active_users: number; |
| 38 | +}; |
| 39 | +
|
| 40 | +const props = defineProps<{ |
| 41 | + buckets: Bucket[]; |
| 42 | + loading: boolean; |
| 43 | + error: string | null; |
| 44 | + emptyMessage?: string; |
| 45 | +}>(); |
| 46 | +
|
| 47 | +const labels = computed(() => |
| 48 | + props.buckets.map((item) => |
| 49 | + new Intl.DateTimeFormat(undefined, { |
| 50 | + month: 'short', |
| 51 | + day: 'numeric', |
| 52 | + hour: '2-digit', |
| 53 | + minute: '2-digit', |
| 54 | + }).format(item.bucket_start * 1000), |
| 55 | + ), |
| 56 | +); |
| 57 | +
|
| 58 | +const datasetValues = computed(() => props.buckets.map((item) => item.active_users)); |
| 59 | +
|
| 60 | +const chartData = computed(() => ({ |
| 61 | + labels: labels.value, |
| 62 | + datasets: [ |
| 63 | + { |
| 64 | + label: 'Active users', |
| 65 | + data: datasetValues.value, |
| 66 | + borderColor: '#38bdf8', |
| 67 | + backgroundColor: '#38bdf8', |
| 68 | + pointRadius: 2, |
| 69 | + borderWidth: 2, |
| 70 | + tension: 0.25, |
| 71 | + }, |
| 72 | + ], |
| 73 | +})); |
| 74 | +
|
| 75 | +const chartOptions = computed(() => ({ |
| 76 | + maintainAspectRatio: false, |
| 77 | + responsive: true, |
| 78 | + plugins: { |
| 79 | + legend: { |
| 80 | + display: false, |
| 81 | + }, |
| 82 | + tooltip: { |
| 83 | + callbacks: { |
| 84 | + label: (ctx: any) => `${ctx.parsed.y} active users`, |
| 85 | + }, |
| 86 | + }, |
| 87 | + }, |
| 88 | + scales: { |
| 89 | + x: { |
| 90 | + ticks: { |
| 91 | + color: '#94a3b8', |
| 92 | + }, |
| 93 | + grid: { |
| 94 | + color: '#1e293b', |
| 95 | + }, |
| 96 | + }, |
| 97 | + y: { |
| 98 | + beginAtZero: true, |
| 99 | + ticks: { |
| 100 | + color: '#94a3b8', |
| 101 | + precision: 0, |
| 102 | + }, |
| 103 | + grid: { |
| 104 | + color: '#1e293b', |
| 105 | + }, |
| 106 | + }, |
| 107 | + }, |
| 108 | +})); |
| 109 | +
|
| 110 | +const emptyMessage = computed(() => props.emptyMessage ?? 'No activity recorded for this period.'); |
| 111 | +</script> |
0 commit comments