Skip to content

Commit 6444c5b

Browse files
Add stacked bar chart of issue reactions by month with bars broken down by reaction type
1 parent 4459561 commit 6444c5b

File tree

2 files changed

+147
-0
lines changed

2 files changed

+147
-0
lines changed

examples/nextjs-import-airbyte-github-export-seafowl/components/RepositoryAnalytics/Charts.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { SqlProvider, makeSeafowlHTTPContext } from "@madatdata/react";
66
import { useMemo } from "react";
77

88
import { StargazersChart } from "./charts/StargazersChart";
9+
import { MonthlyIssueStatsTable } from "./charts/MonthlyIssueStats";
910

1011
export interface ChartsProps {
1112
importedRepository: ImportedRepository;
@@ -29,7 +30,9 @@ export const Charts = ({ importedRepository }: ChartsProps) => {
2930
<SqlProvider dataContext={seafowlDataContext}>
3031
<h3>Stargazers</h3>
3132
<StargazersChart {...importedRepository} />
33+
<MonthlyIssueStatsTable {...importedRepository} />
3234
</SqlProvider>
35+
MonthlyIssueStatsTable
3336
</div>
3437
);
3538
};
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import * as Plot from "@observablehq/plot";
2+
import { useSqlPlot } from "../useSqlPlot";
3+
import type { ImportedRepository, TargetSplitgraphRepo } from "../../../types";
4+
5+
// Assume meta namespace contains both the meta tables, and all imported repositories and tables
6+
const META_NAMESPACE =
7+
process.env.NEXT_PUBLIC_SPLITGRAPH_GITHUB_ANALYTICS_META_NAMESPACE;
8+
9+
type Reaction =
10+
// | "all"
11+
| "plus_one"
12+
| "minus_one"
13+
| "laugh"
14+
| "confused"
15+
| "heart"
16+
| "hooray"
17+
| "rocket"
18+
| "eyes";
19+
20+
type MappedMonthlyIssueStatsRow = MonthlyIssueStatsRow & {
21+
created_at_month: Date;
22+
};
23+
24+
/**
25+
* A stacked bar chart of the number of reactions each month, grouped by reaction type
26+
*/
27+
export const MonthlyIssueStats = ({
28+
splitgraphNamespace,
29+
splitgraphRepository,
30+
}: ImportedRepository) => {
31+
const renderPlot = useSqlPlot({
32+
sqlParams: { splitgraphNamespace, splitgraphRepository },
33+
buildQuery: monthlyIssueStatsTableQuery,
34+
mapRows: (r: MonthlyIssueStatsRow) => ({
35+
...r,
36+
created_at_month: new Date(r.created_at_month),
37+
}),
38+
reduceRows: (rows: MappedMonthlyIssueStatsRow[]) => {
39+
const reactions = new Map<
40+
Reaction,
41+
{ created_at_month: Date; count: number }[]
42+
>();
43+
44+
for (const row of rows) {
45+
for (const reaction of [
46+
"plus_one",
47+
"minus_one",
48+
"laugh",
49+
"confused",
50+
"heart",
51+
"hooray",
52+
"rocket",
53+
"eyes",
54+
] as Reaction[]) {
55+
if (!reactions.has(reaction)) {
56+
reactions.set(reaction, []);
57+
}
58+
59+
reactions.get(reaction)!.push({
60+
created_at_month: row.created_at_month,
61+
count: (() => {
62+
switch (reaction) {
63+
case "plus_one":
64+
return row.total_plus_ones;
65+
case "minus_one":
66+
return row.total_minus_ones;
67+
case "laugh":
68+
return row.total_laughs;
69+
case "confused":
70+
return row.total_confused;
71+
case "heart":
72+
return row.total_hearts;
73+
case "hooray":
74+
return row.total_hoorays;
75+
case "rocket":
76+
return row.total_rockets;
77+
case "eyes":
78+
return row.total_eyes;
79+
}
80+
})(),
81+
});
82+
}
83+
}
84+
85+
return Array.from(reactions.entries()).flatMap(([reaction, series]) =>
86+
series.map((d) => ({ reaction, ...d }))
87+
);
88+
},
89+
isRenderable: (p) => !!p.splitgraphRepository,
90+
91+
makePlotOptions: (issueStats) => ({
92+
y: { grid: true },
93+
color: { legend: true },
94+
marks: [
95+
Plot.rectY(issueStats, {
96+
x: "created_at_month",
97+
y: "count",
98+
interval: "month",
99+
fill: "reaction",
100+
}),
101+
Plot.ruleY([0]),
102+
],
103+
}),
104+
});
105+
106+
return renderPlot();
107+
};
108+
109+
/** Shape of row returned by {@link monthlyIssueStatsTableQuery} */
110+
export type MonthlyIssueStatsRow = {
111+
created_at_month: string;
112+
num_issues: number;
113+
total_reacts: number;
114+
total_plus_ones: number;
115+
total_minus_ones: number;
116+
total_laughs: number;
117+
total_confused: number;
118+
total_hearts: number;
119+
total_hoorays: number;
120+
total_rockets: number;
121+
total_eyes: number;
122+
};
123+
124+
/** Time series of GitHub stargazers for the given repository */
125+
export const monthlyIssueStatsTableQuery = ({
126+
splitgraphNamespace = META_NAMESPACE,
127+
splitgraphRepository,
128+
}: TargetSplitgraphRepo) => {
129+
return `SELECT
130+
created_at_month,
131+
count(issue_number) as num_issues,
132+
sum(total_reacts) as total_reacts,
133+
sum(no_plus_one) as total_plus_ones,
134+
sum(no_minus_one) as total_minus_ones,
135+
sum(no_laugh) as total_laughs,
136+
sum(no_confused) as total_confused,
137+
sum(no_heart) as total_hearts,
138+
sum(no_hooray) as total_hoorays,
139+
sum(no_rocket) as total_rockets,
140+
sum(no_eyes) as total_eyes
141+
FROM "${splitgraphNamespace}/${splitgraphRepository}"."monthly_issue_stats"
142+
GROUP BY created_at_month
143+
ORDER BY created_at_month ASC;`;
144+
};

0 commit comments

Comments
 (0)