File size: 5,936 Bytes
e95333f
32f54e4
e95333f
32f54e4
 
 
 
 
e95333f
32f54e4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e95333f
32f54e4
 
 
 
 
 
c700b14
 
 
32f54e4
 
 
 
c700b14
32f54e4
 
c700b14
32f54e4
 
 
 
 
c700b14
32f54e4
c700b14
32f54e4
c700b14
 
32f54e4
 
 
c700b14
32f54e4
 
 
 
 
c700b14
32f54e4
 
c700b14
 
 
 
32f54e4
 
 
 
 
c700b14
 
 
 
32f54e4
 
 
 
 
c700b14
32f54e4
 
c700b14
32f54e4
 
 
 
 
c700b14
32f54e4
 
c700b14
32f54e4
 
 
 
 
 
 
 
c700b14
32f54e4
 
 
 
 
 
 
 
 
 
 
 
 
c700b14
 
 
 
32f54e4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e95333f
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
<!doctype html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title>GitHub Stars Trending Score</title>
		<script src="https://cdn.tailwindcss.com"></script>
		<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
	</head>
	<body class="bg-gray-50 p-8 text-gray-900">
		<h1 class="mb-4 text-2xl font-semibold">GitHub Stars & Trending Score (1 Week)</h1>
		<p class="mb-6 text-gray-600">
			This demo shows how GitHub star activity changes over time with a trending score calculated using exponential
			moving average (EMA).<br />
			Adjust the alpha parameter to see how it affects the smoothing of the trending score relative to raw star counts.
		</p>

		<div class="mb-6 flex flex-wrap gap-4">
			<div>
				<label class="mb-2 block text-sm font-medium text-gray-700">
					Alpha (EMA weight): <span id="alphaValue" class="ml-2 font-bold">0.40</span>
				</label>
				<input id="alphaSlider" type="range" min="0" max="1" step="0.01" value="0.4" class="w-64" />
			</div>
			<div>
				<label class="mb-2 block text-sm font-medium text-gray-700">&nbsp;</label>
				<button id="generateData" class="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
					Generate Data
				</button>
			</div>
		</div>

		<div class="h-96 w-full rounded-lg bg-white shadow-sm">
			<canvas id="trendChart" class="h-full w-full"></canvas>
		</div>

		<script>
			const UPDATES_PER_DAY = 8; // Every 3 hours
			const DAYS = 7;
			const TOTAL_UPDATES = UPDATES_PER_DAY * DAYS; // 56 updates total

			function generateRealisticData() {
				const patterns = [
					"consistent_growth",
					"flat", 
					"declining",
					"spike_early",
					"spike_late", 
					"gradual_increase",
					"volatile",
				];

				const pattern = patterns[Math.floor(Math.random() * patterns.length)];
				const starsPerHourData = [];

				for (let update = 0; update < TOTAL_UPDATES; update++) {
					let value;
					const progress = update / TOTAL_UPDATES; // 0 to 1 over the week
					const noise = (Math.random() - 0.5) * 0.5;

					switch (pattern) {
						case "consistent_growth":
							value = 0.5 + progress * 3 + noise;
							break;
						case "flat":
							value = 1 + noise;
							break;
						case "declining":
							value = 3 - progress * 2.5 + noise;
							break;
						case "spike_early":
							// Spike in first day (updates 0-7)
							if (update >= 2 && update <= 10) {
								const distance = Math.abs(update - 6);
								value = 1 + 15 * Math.max(0, 1 - distance / 4) + noise;
							} else {
								value = 1 + noise;
							}
							break;
						case "spike_late":
							// Spike in last day (updates 48-55)
							if (update >= 44 && update <= 52) {
								const distance = Math.abs(update - 48);
								value = 1 + 12 * Math.max(0, 1 - distance / 4) + noise;
							} else {
								value = 1 + noise;
							}
							break;
						case "gradual_increase":
							value = 0.3 + Math.pow(progress, 1.5) * 4 + noise;
							break;
						case "volatile":
							value = 1 + Math.sin(update * 0.8) * 2 + Math.cos(update * 0.3) * 1 + noise;
							break;
						default:
							value = 1 + noise;
					}

					starsPerHourData.push(Math.max(0, value));
				}

				return starsPerHourData;
			}

			let starsPerHour = generateRealisticData();

			function calculateTrendingScore(alpha) {
				const scores = [starsPerHour[0]];

				for (let i = 1; i < starsPerHour.length; i++) {
					// Simple EMA: each point is a stars-per-hour rate
					scores[i] = alpha * starsPerHour[i] + (1 - alpha) * scores[i - 1];
				}

				return scores;
			}

			const slider = document.getElementById("alphaSlider");
			const alphaDisplay = document.getElementById("alphaValue");
			const generateButton = document.getElementById("generateData");

			let alpha = parseFloat(slider.value);
			let trendingScores = calculateTrendingScore(alpha);

			const labels = Array.from({ length: TOTAL_UPDATES }, (_, i) => {
				const day = Math.floor(i / UPDATES_PER_DAY) + 1;
				return `Day ${day}`;
			});

			const chart = new Chart(document.getElementById("trendChart"), {
				type: "line",
				data: {
					labels,
					datasets: [
						{
							label:           "Stars per hour",
							data:            starsPerHour,
							type:            "bar",
							borderColor:     "#3b82f6",
							backgroundColor: "rgba(59, 130, 246, 0.6)",
							yAxisID:         "y",
						},
						{
							label:           "Trending score",
							data:            trendingScores,
							type:            "line",
							borderColor:     "#ef4444",
							backgroundColor: "rgba(239, 68, 68, 0.1)",
							fill:            false,
							tension:         0.3,
							yAxisID:         "y",
						},
					],
				},
				options: {
					responsive:          true,
					maintainAspectRatio: false,
					interaction:         { mode: "index" },
					scales:              {
						y: {
							beginAtZero: true,
							title:       {
								display: true,
								text:    "Stars / Score",
							},
						},
						x: {
							title: {
								display: true,
								text:    "Days since launch",
							},
						},
					},
					plugins: {
						tooltip: {
							callbacks: {
								label: context => `${context.dataset.label}: ${context.parsed.y.toFixed(1)}`,
							},
						},
					},
				},
			});

			slider.addEventListener("input", () => {
				alpha = parseFloat(slider.value);
				alphaDisplay.textContent = alpha.toFixed(2);
				trendingScores = calculateTrendingScore(alpha);
				chart.data.datasets[1].data = trendingScores;
				chart.update();
			});

			generateButton.addEventListener("click", () => {
				starsPerHour = generateRealisticData();
				trendingScores = calculateTrendingScore(alpha);
				chart.data.datasets[0].data = starsPerHour;
				chart.data.datasets[1].data = trendingScores;
				chart.update();
			});
		</script>
	</body>
</html>