Skip to content

Commit

Permalink
Render stargazers line chart with Observable Plot querying Seafowl
Browse files Browse the repository at this point in the history
  • Loading branch information
milesrichardson committed May 30, 2023

Unverified

This user has not yet uploaded their public signing key.
1 parent e297d17 commit e222758
Showing 7 changed files with 337 additions and 61 deletions.
Original file line number Diff line number Diff line change
@@ -1,31 +1,102 @@
import style from "./Charts.module.css";
import { useEffect, useRef } from "react";

import type { ImportedRepository } from "../../types";
import { SqlProvider, makeSeafowlHTTPContext, useSql } from "@madatdata/react";

import * as Plot from "@observablehq/plot";
import { useMemo } from "react";

import {
stargazersLineChartQuery,
type StargazersLineChartRow,
} from "./sql-queries";

export interface ChartsProps {
importedRepository: ImportedRepository;
}

export const Charts = ({
importedRepository: {
githubNamespace,
githubRepository,
splitgraphNamespace,
splitgraphRepository,
},
}: ChartsProps) => {
// 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;

export const Charts = ({ importedRepository }: ChartsProps) => {
const seafowlDataContext = useMemo(
() =>
makeSeafowlHTTPContext("https://demo.seafowl.cloud", {
dbname: META_NAMESPACE,
}),
[]
);

return (
<div className={style.charts}>
Chart for{" "}
<a href={`https://github.com/${githubNamespace}/${githubRepository}`}>
github.com/{githubNamespace}/{githubRepository}
</a>
, based on{" "}
<a
href={`https://www.splitgraph.com/${splitgraphNamespace}/${splitgraphRepository}`}
>
splitgraph.com/{splitgraphNamespace}/{splitgraphRepository}
</a>
<SqlProvider dataContext={seafowlDataContext}>
<StargazersChart {...importedRepository} />
</SqlProvider>
</div>
);
};

const StargazersChart = ({
splitgraphNamespace,
splitgraphRepository,
}: ImportedRepository) => {
const containerRef = useRef<HTMLDivElement>();

const { response, error } = useSql<StargazersLineChartRow>(
stargazersLineChartQuery({ splitgraphNamespace, splitgraphRepository })
);

const stargazers = useMemo(() => {
return !response || error
? []
: (response.rows ?? []).map((r) => ({
...r,
starred_at: new Date(r.starred_at),
}));
}, [response, error]);

useEffect(() => {
if (stargazers === undefined) {
return;
}

const plot = Plot.plot({
y: { grid: true },
color: { scheme: "burd" },
marks: [
Plot.lineY(stargazers, {
x: "starred_at",
y: "cumulative_stars",
}),
// NOTE: We don't have username when querying Seafowl because it's within a JSON object,
// and seafowl doesn't support querying inside JSON objects
// Plot.tip(
// stargazers,
// Plot.pointer({
// x: "starred_at",
// y: "cumulative_stars",
// title: (d) => `${d.username} was stargazer #${d.cumulative_stars}`,
// })
// ),
],
});

// There is a bug(?) in useSql where, since we can't give it dependencies, it
// will re-run even with splitgraphNamespace and splitgraphRepository are undefined,
// which results in an error querying Seafowl. So just don't render the chart in that case.
if (splitgraphNamespace && splitgraphRepository) {
containerRef.current.append(plot);
}

return () => plot.remove();
}, [stargazers]);

return (
<>
<h3>Stargazers</h3>
<div ref={containerRef} />
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.importedRepoMetadata {
background: inherit;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import type { ImportedRepository } from "../../types";
import style from "./ImportedRepoMetadata.module.css";

import { makeStargazersTableQuery } from "./sql-queries";

// 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;

interface ImportedRepoMetadataProps {
importedRepository: ImportedRepository;
}

export const ImportedRepoMetadata = ({
importedRepository,
}: ImportedRepoMetadataProps) => {
return (
<div className={style.importedRepoMetadata}>
<h1>
<GitHubRepoLink {...importedRepository} />
</h1>
<h2>GitHub Analytics</h2>

<ul>
<li>
Browse the data: <SplitgraphRepoLink {...importedRepository} />
</li>
<li>
<SplitgraphQueryLink
importedRepository={importedRepository}
tableName={"stargazers"}
makeQuery={makeStargazersTableQuery}
/>
</li>
<li>
<SeafowlQueryLink
importedRepository={importedRepository}
tableName={"stargazers"}
makeQuery={makeStargazersTableQuery}
/>
</li>
</ul>
</div>
);
};

const SplitgraphRepoLink = ({
splitgraphNamespace,
splitgraphRepository,
}: ImportedRepository) => {
return (
<a
target="_blank"
href={`https://www.splitgraph.com/${splitgraphNamespace}/${splitgraphRepository}`}
>
{`splitgraph.com/${splitgraphNamespace}/${splitgraphRepository}`}
</a>
);
};

const GitHubRepoLink = ({
githubNamespace,
githubRepository,
}: ImportedRepository) => {
return (
<a
target="_blank"
href={`https://github.com/${githubNamespace}/${githubRepository}`}
>{`github.com/${githubNamespace}/${githubRepository}`}</a>
);
};

const SplitgraphQueryLink = ({
importedRepository,
makeQuery,
tableName,
}: {
importedRepository: ImportedRepository;
makeQuery: (repo: ImportedRepository) => string;
tableName: string;
}) => {
return (
<a
href={makeSplitgraphQueryHref(makeQuery(importedRepository))}
target="_blank"
>
Query {tableName} in the Splitgraph Console
</a>
);
};

const SeafowlQueryLink = ({
importedRepository,
makeQuery,
tableName,
}: {
importedRepository: ImportedRepository;
makeQuery: (repo: ImportedRepository) => string;
tableName: string;
}) => {
return (
<a
href={makeSeafowlQueryHref(makeQuery(importedRepository))}
target="_blank"
>
Query Seafowl table {tableName} using the Splitgraph Console
</a>
);
};

/** Return the URL to Splitgraph Console pointing to Splitgraph DDN */
const makeSplitgraphQueryHref = (sqlQuery: string) => {
const url = `https://www.splitgraph.com/query?${new URLSearchParams({
sqlQuery: sqlQuery,
flavor: "splitgraph",
}).toString()}`;

return url;
};

/** Return the URL to Splitgraph Console pointing to Seafowl db where we export tables */
const makeSeafowlQueryHref = (sqlQuery: string) => {
return `https://www.splitgraph.com/query?${new URLSearchParams({
sqlQuery: sqlQuery,
flavor: "seafowl",
// Splitgraph exports to Seafowl dbname matching the username of the exporting user
"database-name": META_NAMESPACE,
})}`;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { ImportedRepository } 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 TargetSplitgraphRepo = {
splitgraphNamespace?: string;
splitgraphRepository: string;
};

/**
* Raw query to select all columns in the stargazers table, which can be
* run on both Splitgraph and Seafowl.
*
* This is meant for linking to the query editor, not for rendering charts.
*/
export const makeStargazersTableQuery = ({
splitgraphNamespace = META_NAMESPACE,
splitgraphRepository,
}: TargetSplitgraphRepo) => {
return `SELECT
"repository",
"user_id",
"starred_at",
"user",
"_airbyte_ab_id",
"_airbyte_emitted_at",
"_airbyte_normalized_at",
"_airbyte_stargazers_hashid"
FROM
"${splitgraphNamespace}/${splitgraphRepository}"."stargazers"
LIMIT 100;`;
};

/** Shape of row returned by {@link stargazersLineChartQuery} */
export type StargazersLineChartRow = {
username: string;
cumulative_stars: number;
starred_at: string;
};

/** Time series of GitHub stargazers for the given repository */
export const stargazersLineChartQuery = ({
splitgraphNamespace = META_NAMESPACE,
splitgraphRepository,
}: TargetSplitgraphRepo) => {
return `SELECT
COUNT(*) OVER (ORDER BY starred_at) AS cumulative_stars,
starred_at
FROM
"${splitgraphNamespace}/${splitgraphRepository}"."stargazers"
ORDER BY starred_at;`;
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useMemo } from "react";
import Link from "next/link";
import styles from "./Sidebar.module.css";
import { SqlProvider, makeSplitgraphHTTPContext } from "@madatdata/react";
import { useSql } from "@madatdata/react";

import type { ImportedRepository } from "../types";
@@ -46,7 +47,6 @@ const useImportedRepositories = (): ImportedRepository[] => {
}

if (!response) {
console.warn("No response received");
return [];
}

@@ -56,27 +56,40 @@ const useImportedRepositories = (): ImportedRepository[] => {
return repositories;
};

export const Sidebar = () => {
const RepositoriesList = () => {
const repositories = useImportedRepositories();

return (
<ul className={styles.repoList}>
{repositories.map((repo, index) => (
<li key={index}>
<Link
href={`/${repo.githubNamespace}/${repo.githubRepository}?splitgraphNamespace=${repo.splitgraphNamespace}&splitgraphRepository=${repo.splitgraphRepository}`}
>
{repo.githubNamespace}/{repo.githubRepository}
</Link>
</li>
))}
</ul>
);
};

export const Sidebar = () => {
const splitgraphDataContext = useMemo(
() => makeSplitgraphHTTPContext({ credential: null }),
[]
);

return (
<aside className={styles.sidebar}>
<div className={styles.importButtonContainer}>
<Link href="/" className={styles.importButton}>
Import Your Repository
</Link>
</div>
<ul className={styles.repoList}>
{repositories.map((repo, index) => (
<li key={index}>
<Link
href={`/${repo.githubNamespace}/${repo.githubRepository}?splitgraphNamespace=${repo.splitgraphNamespace}&splitgraphRepository=${repo.splitgraphRepository}`}
>
{repo.githubNamespace}/{repo.githubRepository}
</Link>
</li>
))}
</ul>
<SqlProvider dataContext={splitgraphDataContext}>
<RepositoriesList />
</SqlProvider>
</aside>
);
};
Loading

0 comments on commit e222758

Please sign in to comment.