Skip to content

Commit 715814a

Browse files
feat: package heatmap (#195)
* fix: cache values * fix: empty dependency render * wip * wip * wip * chore: revise heatmap computations * chore: self review * chore: bump version --------- Co-authored-by: Tom Schönmann <[email protected]>
1 parent 3bbcd0d commit 715814a

18 files changed

+430
-259
lines changed

web/app/data/package-insight.service.server.ts

Lines changed: 36 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,133 +1,66 @@
1-
import { format, sub } from "date-fns";
1+
import { addHours, format } from "date-fns";
2+
import { ExpiringSearchIndex } from "./types";
23
import {
34
CranDownloadsResponse,
45
CranResponse,
56
CranTopDownloadedPackagesRes,
67
CranTrendingPackagesRes,
7-
PackageDownloadTrend,
8-
} from "./types";
9-
import { TopDownloadedPackagesRange } from "./package-insight.shape";
8+
TopDownloadedPackagesRange,
9+
} from "./package-insight.shape";
1010
import { slog } from "../modules/observability.server";
1111

1212
export class PackageInsightService {
1313
private static readonly CRAN_LOGS_URL = "https://cranlogs.r-pkg.org";
1414

15-
/**
16-
* Get the downloads for a package in the last n days, starting
17-
* from today. The result is an array of objects with the number
18-
* of downloads, the relative trend for the respective time period
19-
* and the label.
20-
*
21-
* @param name The name of the package.
22-
* @returns The downloads for the package.
23-
*/
24-
static async getDownloadsWithTrends(
25-
name: string,
26-
): Promise<PackageDownloadTrend[]> {
27-
const getDownloads = async (days: number, from?: Date) => {
28-
const res = await this.getPackageDownloadsLastNDays({
29-
name,
30-
days,
31-
from,
32-
});
33-
return res?.[0]?.downloads;
15+
private static trendingPackages: ExpiringSearchIndex<CranTrendingPackagesRes> =
16+
{
17+
index: [],
18+
expiresAt: 0,
3419
};
3520

36-
// Fetch all statistics in parallel.
37-
const now = new Date();
38-
const [stats, trendReferences] = await Promise.all([
39-
Promise.all([
40-
getDownloads(1),
41-
getDownloads(7, now),
42-
getDownloads(30, now),
43-
getDownloads(90, now),
44-
getDownloads(365, now),
45-
]),
46-
Promise.all([
47-
getDownloads(0, sub(now, { days: 2 })),
48-
getDownloads(7, sub(now, { days: 7 })),
49-
getDownloads(30, sub(now, { days: 30 })),
50-
getDownloads(90, sub(now, { days: 90 })),
51-
getDownloads(365, sub(now, { days: 365 })),
52-
]),
53-
]);
54-
55-
// Get rend in percentage.
56-
const trends = stats.map((stat, i) => {
57-
const ref = trendReferences[i];
58-
// No valid values.
59-
if (stat === undefined || ref === undefined || ref === 0) {
60-
return "";
61-
}
62-
const diff = stat - ref;
63-
return `${diff > 0 ? "+" : ""}${((diff / ref) * 100).toFixed(0)}%`;
64-
});
65-
66-
// Aggregate the statistics into a single object.
67-
const labels = [
68-
"Yesterday",
69-
"Last 7 days",
70-
"Last 30 days",
71-
"Last 90 days",
72-
"Last 365 days",
73-
];
74-
const downloads = stats
75-
.map((value, i) => ({
76-
value,
77-
trend: trends[i],
78-
label: labels[i],
79-
}))
80-
.filter(({ value }) => value !== undefined)
81-
.map(({ value, ...rest }) => ({
82-
value: this.format1kDelimiter(value),
83-
...rest,
84-
}));
85-
86-
return downloads;
87-
}
88-
8921
static async getTopDownloadedPackages(
9022
period: TopDownloadedPackagesRange,
9123
count: number,
9224
) {
93-
return this.fetchFromCRAN<CranTopDownloadedPackagesRes>(
25+
return await this.fetchFromCRAN<CranTopDownloadedPackagesRes>(
9426
`/top/${period}/${count.toString()}`,
9527
);
9628
}
9729

9830
static async getTrendingPackages() {
99-
// Only for last week.
100-
const data = await this.fetchFromCRAN<CranTrendingPackagesRes>("/trending");
101-
return data.map((item) => ({
102-
...item,
103-
increase: `${new Number(item.increase).toFixed(0)}%`,
104-
}));
105-
}
106-
107-
/*
108-
* Private.
109-
*/
110-
111-
private static async getPackageDownloadsLastNDays(params: {
112-
name: string;
113-
days: number;
114-
from?: Date;
115-
}) {
116-
const { name, days, from } = params;
31+
// Check if the index has expired.
32+
if (this.trendingPackages.expiresAt < Date.now()) {
33+
// Only for last week.
34+
const data = await this.fetchFromCRAN<CranTrendingPackagesRes>(
35+
"/trending",
36+
).then((data) => {
37+
return data.map((item) => ({
38+
...item,
39+
increase: `${new Number(item.increase).toFixed(0)}%`,
40+
}));
41+
});
11742

118-
// Special case as the logs-API returns data earliest for
119-
// the last day according to its point of reference (likely UTC).
120-
if (days === 1 && !from) {
121-
return this
122-
.fetchLogsFromCRAN<CranDownloadsResponse>`/downloads/total/last-day/${name}`;
43+
this.trendingPackages = {
44+
index: data,
45+
expiresAt: addHours(new Date(), 6).getTime(),
46+
};
12347
}
12448

125-
const validFrom = from || new Date();
126-
const past = sub(validFrom, { days });
49+
return this.trendingPackages.index;
50+
}
51+
52+
static async getDailyDownloadsForPackage(
53+
name: string,
54+
range: TopDownloadedPackagesRange,
55+
): Promise<CranDownloadsResponse> {
12756
return this
128-
.fetchLogsFromCRAN<CranDownloadsResponse>`/downloads/total/${past}:${validFrom}/${name}`;
57+
.fetchLogsFromCRAN<CranDownloadsResponse>`/downloads/daily/${range}/${name}`;
12958
}
13059

60+
/*
61+
* Private.
62+
*/
63+
13164
private static async fetchFromCRAN<R extends CranResponse = CranResponse>(
13265
url: string,
13366
): Promise<R> {

web/app/data/package-insight.shape.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,55 @@ export const topDownloadedPackagesRangeSchema = z.union([
99
export type TopDownloadedPackagesRange = z.infer<
1010
typeof topDownloadedPackagesRangeSchema
1111
>;
12+
13+
export const cranDownloadsResponseSchema = z.array(
14+
z.object({
15+
downloads: z.array(
16+
z.object({
17+
day: z.string(),
18+
downloads: z.number(),
19+
}),
20+
),
21+
start: z.string(),
22+
end: z.string(),
23+
package: z.string(),
24+
}),
25+
);
26+
27+
export type CranDownloadsResponse = z.infer<typeof cranDownloadsResponseSchema>;
28+
29+
export const cranTopDownloadedPackagesResSchema = z.object({
30+
start: z.string(),
31+
end: z.string(),
32+
downloads: z.array(
33+
z.object({
34+
package: z.string(),
35+
downloads: z.number(),
36+
}),
37+
),
38+
});
39+
40+
export type CranTopDownloadedPackagesRes = z.infer<
41+
typeof cranTopDownloadedPackagesResSchema
42+
>;
43+
44+
/**
45+
* Trending packages are the ones that were downloaded at least 1000 times during last week,
46+
* and that substantially increased their download counts, compared to the average weekly downloads in the previous 24 weeks.
47+
* The percentage of increase is also shown in the output.
48+
*/
49+
export const cranTrendingPackagesResSchema = z.array(
50+
z.object({
51+
package: z.string(),
52+
increase: z.string(),
53+
}),
54+
);
55+
56+
export type CranTrendingPackagesRes = z.infer<
57+
typeof cranTrendingPackagesResSchema
58+
>;
59+
60+
export type CranResponse =
61+
| CranDownloadsResponse
62+
| CranTopDownloadedPackagesRes
63+
| CranTrendingPackagesRes;

web/app/data/types.ts

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -112,37 +112,3 @@ export type ExpiringSearchIndex<T> = {
112112
index: T;
113113
expiresAt: number;
114114
};
115-
116-
export type CranDownloadsResponse = Array<{
117-
downloads: number;
118-
start: string;
119-
end: string;
120-
package: string;
121-
}>;
122-
123-
export type CranTopDownloadedPackagesRes = {
124-
start: string; // e.g. "2015-05-01T00:00:00.000Z";
125-
end: string; // e.g. "2015-05-01T00:00:00.000Z";
126-
downloads: Array<{ package: string; downloads: number }>;
127-
};
128-
129-
/**
130-
* Trending packages are the ones that were downloaded at least 1000 times during last week,
131-
* and that substantially increased their download counts, compared to the average weekly downloads in the previous 24 weeks.
132-
* The percentage of increase is also shown in the output.
133-
*/
134-
export type CranTrendingPackagesRes = Array<{
135-
package: string;
136-
increase: number;
137-
}>;
138-
139-
export type CranResponse =
140-
| CranDownloadsResponse
141-
| CranTopDownloadedPackagesRes
142-
| CranTrendingPackagesRes;
143-
144-
export type PackageDownloadTrend = {
145-
trend: string;
146-
label: string;
147-
value: string;
148-
};

web/app/modules/contact-pill.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,13 @@ export function ContactPill(props: Props) {
5656
{isMaintainer ? (
5757
<InfoPill
5858
size="sm"
59-
label={<RiVipCrown2Fill size={16} className="text-gold-2" />}
60-
className="border-transparent bg-gold-11 text-gold-1 dark:bg-gold-12"
59+
label={
60+
<RiVipCrown2Fill
61+
size={16}
62+
className="text-gold-10 dark:text-gold-2"
63+
/>
64+
}
65+
className="border-transparent bg-gold-4 text-gold-12 dark:bg-gold-12 dark:text-gold-1"
6166
>
6267
Maintainer
6368
</InfoPill>

0 commit comments

Comments
 (0)