diff --git a/examples/nextjs-import-airbyte-github-export-seafowl/components/RepositoryAnalytics/Charts.tsx b/examples/nextjs-import-airbyte-github-export-seafowl/components/RepositoryAnalytics/Charts.tsx index 6eea242..93c8674 100644 --- a/examples/nextjs-import-airbyte-github-export-seafowl/components/RepositoryAnalytics/Charts.tsx +++ b/examples/nextjs-import-airbyte-github-export-seafowl/components/RepositoryAnalytics/Charts.tsx @@ -6,6 +6,7 @@ import { SqlProvider, makeSeafowlHTTPContext } from "@madatdata/react"; import { useMemo } from "react"; import { StargazersChart } from "./charts/StargazersChart"; +import { MonthlyIssueStatsTable } from "./charts/MonthlyIssueStats"; export interface ChartsProps { importedRepository: ImportedRepository; @@ -29,7 +30,9 @@ export const Charts = ({ importedRepository }: ChartsProps) => {

Stargazers

+
+ MonthlyIssueStatsTable ); }; diff --git a/examples/nextjs-import-airbyte-github-export-seafowl/components/RepositoryAnalytics/charts/MonthlyIssueStats.tsx b/examples/nextjs-import-airbyte-github-export-seafowl/components/RepositoryAnalytics/charts/MonthlyIssueStats.tsx new file mode 100644 index 0000000..f6ce62e --- /dev/null +++ b/examples/nextjs-import-airbyte-github-export-seafowl/components/RepositoryAnalytics/charts/MonthlyIssueStats.tsx @@ -0,0 +1,144 @@ +import * as Plot from "@observablehq/plot"; +import { useSqlPlot } from "../useSqlPlot"; +import type { ImportedRepository, TargetSplitgraphRepo } from "../../../types"; + +// Assume meta namespace contains both the meta tables, and all imported repositories and tables +const META_NAMESPACE = + process.env.NEXT_PUBLIC_SPLITGRAPH_GITHUB_ANALYTICS_META_NAMESPACE; + +type Reaction = + // | "all" + | "plus_one" + | "minus_one" + | "laugh" + | "confused" + | "heart" + | "hooray" + | "rocket" + | "eyes"; + +type MappedMonthlyIssueStatsRow = MonthlyIssueStatsRow & { + created_at_month: Date; +}; + +/** + * A stacked bar chart of the number of reactions each month, grouped by reaction type + */ +export const MonthlyIssueStats = ({ + splitgraphNamespace, + splitgraphRepository, +}: ImportedRepository) => { + const renderPlot = useSqlPlot({ + sqlParams: { splitgraphNamespace, splitgraphRepository }, + buildQuery: monthlyIssueStatsTableQuery, + mapRows: (r: MonthlyIssueStatsRow) => ({ + ...r, + created_at_month: new Date(r.created_at_month), + }), + reduceRows: (rows: MappedMonthlyIssueStatsRow[]) => { + const reactions = new Map< + Reaction, + { created_at_month: Date; count: number }[] + >(); + + for (const row of rows) { + for (const reaction of [ + "plus_one", + "minus_one", + "laugh", + "confused", + "heart", + "hooray", + "rocket", + "eyes", + ] as Reaction[]) { + if (!reactions.has(reaction)) { + reactions.set(reaction, []); + } + + reactions.get(reaction)!.push({ + created_at_month: row.created_at_month, + count: (() => { + switch (reaction) { + case "plus_one": + return row.total_plus_ones; + case "minus_one": + return row.total_minus_ones; + case "laugh": + return row.total_laughs; + case "confused": + return row.total_confused; + case "heart": + return row.total_hearts; + case "hooray": + return row.total_hoorays; + case "rocket": + return row.total_rockets; + case "eyes": + return row.total_eyes; + } + })(), + }); + } + } + + return Array.from(reactions.entries()).flatMap(([reaction, series]) => + series.map((d) => ({ reaction, ...d })) + ); + }, + isRenderable: (p) => !!p.splitgraphRepository, + + makePlotOptions: (issueStats) => ({ + y: { grid: true }, + color: { legend: true }, + marks: [ + Plot.rectY(issueStats, { + x: "created_at_month", + y: "count", + interval: "month", + fill: "reaction", + }), + Plot.ruleY([0]), + ], + }), + }); + + return renderPlot(); +}; + +/** Shape of row returned by {@link monthlyIssueStatsTableQuery} */ +export type MonthlyIssueStatsRow = { + created_at_month: string; + num_issues: number; + total_reacts: number; + total_plus_ones: number; + total_minus_ones: number; + total_laughs: number; + total_confused: number; + total_hearts: number; + total_hoorays: number; + total_rockets: number; + total_eyes: number; +}; + +/** Time series of GitHub stargazers for the given repository */ +export const monthlyIssueStatsTableQuery = ({ + splitgraphNamespace = META_NAMESPACE, + splitgraphRepository, +}: TargetSplitgraphRepo) => { + return `SELECT + created_at_month, + count(issue_number) as num_issues, + sum(total_reacts) as total_reacts, + sum(no_plus_one) as total_plus_ones, + sum(no_minus_one) as total_minus_ones, + sum(no_laugh) as total_laughs, + sum(no_confused) as total_confused, + sum(no_heart) as total_hearts, + sum(no_hooray) as total_hoorays, + sum(no_rocket) as total_rockets, + sum(no_eyes) as total_eyes +FROM "${splitgraphNamespace}/${splitgraphRepository}"."monthly_issue_stats" +GROUP BY created_at_month +ORDER BY created_at_month ASC;`; +};