Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add data exploring to internal metrics page. #103

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 205 additions & 0 deletions src/pages/internal/metrics/[metricId]/[regionSlug]/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import {
Breadcrumbs,
CircularProgress,
Container,
Link,
Paper,
Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography,
} from "@mui/material";
import type { GetStaticPaths, GetStaticProps, NextPage } from "next";
import { ParsedUrlQuery } from "querystring";

import {
MetricData,
MetricLineChart,
MetricLineThresholdChart,
isoDateOnlyString,
useData,
} from "@actnowcoalition/actnow.js";

import { cms } from "../../../../../cms";
import { PageMetaTags } from "../../../../../components/SocialMetaTags";
import { getRegionFromSlugStrict } from "src/pages/us/[regionSlug]";
import { metricCatalog } from "src/utils/metrics";
import { regions } from "src/utils/regions";
import { getRegionSlug } from "src/utils/routing";

const MetricPage: NextPage<{ regionId?: string; metricId?: string }> = ({
regionId,
metricId,
}) => {
metricId = metricId ?? metricCatalog.metrics[0].id;
regionId = regionId ?? regions.all[0].regionId;

const metric = metricCatalog.getMetric(metricId);
const region = regions.findByRegionIdStrict(regionId);
const metricFullName = metric.extendedName
? `${metric.name}: ${metric.extendedName}`
: metric.name;

// TODO(michael): Fetching timeseries for all regions could be expensive. We might want to make this optional.
const { data, error } = useData(region, metric, /*includeTimeseries=*/ true);

const thresholds = metric.categoryThresholds;
const categories = metric.categorySet?.categories;

const haveTimeseries = data?.hasTimeseries() && data?.timeseries?.hasData();
const useThresholdChart = thresholds?.length && categories;

return (
<>
<PageMetaTags
siteName={cms.settings.siteName}
title={`Metrics: ${metric.id}`}
description={metricFullName}
url={`/internal/metrics/${metric.id}/${getRegionSlug(region)}`}
/>
<Container maxWidth="md" sx={{ py: 5 }}>
{/* TODO: Implement a basic version in the packages repo */}
<Breadcrumbs aria-label="breadcrumb" sx={{ my: 2 }}>
<Link color="inherit" underline="hover" href="/">
Home
</Link>
<Link color="inherit" underline="hover" href="/internal">
Internal
</Link>
<Link color="text.primary" underline="hover" href="/internal/metrics">
Metrics
</Link>
<Link
color="text.primary"
underline="hover"
href={`/internal/metrics/${metricId}`}
>
{metricFullName}
</Link>
<Link
color="text.primary"
underline="hover"
href={`/internal/metrics/${metricId}/${getRegionSlug(region)}`}
>
{region.fullName}
</Link>
</Breadcrumbs>

<Typography>Metric ID: {metric.id}</Typography>
<Typography>Metric Name: {metricFullName}</Typography>
<Typography>Region: {region.fullName}</Typography>
{error && (
<Stack>
<Typography>Failed to fetch data:</Typography>
<pre>error</pre>
<Typography>
More details may be available in the console.
</Typography>
</Stack>
)}

{data && (
<Stack>
<Typography variant="h2">Data</Typography>
<Typography>
<>Current value: {data?.formatValue()}</>
{haveTimeseries && (
<Stack>
<Typography variant="h3" mt={1}>
Chart
</Typography>
{useThresholdChart ? (
<MetricLineThresholdChart
region={region}
metric={metric}
width={600}
height={300}
/>
) : (
<MetricLineChart
region={region}
metric={metric}
width={600}
height={300}
/>
)}

<Typography variant="h3" mt={2} mb={1}>
Raw Data
</Typography>
<DataTable data={data} />
</Stack>
)}
</Typography>
</Stack>
)}

{!data && !error && <CircularProgress />}
</Container>
</>
);
};

const DataTable = ({ data }: { data: MetricData }) => {
return (
<TableContainer component={Paper} sx={{ width: 300 }}>
<Table>
<TableHead>
<TableRow>
<TableCell>Date</TableCell>
<TableCell align="right">Value</TableCell>
</TableRow>
</TableHead>
<TableBody>
{data.timeseries.points.map((row) => (
<TableRow
key={row.date.toISOString()}
sx={{
"&:last-child td, &:last-child th": { border: 0 },
}}
>
<TableCell component="th" scope="row">
{isoDateOnlyString(row.date)}
</TableCell>
<TableCell align="right">
{data.metric.formatValue(row.value)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
};

interface MetricRegionPageParams extends ParsedUrlQuery {
regionSlug: string;
metricId: string;
}

export const getStaticProps: GetStaticProps = async ({ params }) => {
const { metricId, regionSlug } = params as MetricRegionPageParams;
const region = getRegionFromSlugStrict(regionSlug);
return { props: { metricId, regionId: region.regionId } };
};

export const getStaticPaths: GetStaticPaths = async () => {
// Prerendering pages for all metric+region combinations is slow and can cause
// failed builds. So we use fallback rendering.
return { paths: [], fallback: true };

// const paths = regions.all
// .map((region) =>
// metricCatalog.metrics.map((metric) => ({
// params: { regionSlug: getRegionSlug(region), metricId: metric.id },
// }))
// )
// .flat();
// return { paths, fallback: false };
};

export default MetricPage;
192 changes: 192 additions & 0 deletions src/pages/internal/metrics/[metricId]/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import {
Breadcrumbs,
CircularProgress,
Container,
Link,
Paper,
Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography,
} from "@mui/material";
import type { GetStaticPaths, GetStaticProps, NextPage } from "next";
import { ParsedUrlQuery } from "querystring";

import {
Metric,
MetricData,
MultiRegionMultiMetricDataStore,
isoDateOnlyString,
useDataForRegionsAndMetrics,
} from "@actnowcoalition/actnow.js";

import { cms } from "../../../../cms";
import { PageMetaTags } from "../../../../components/SocialMetaTags";
import { metricCatalog } from "src/utils/metrics";
import { regions } from "src/utils/regions";
import { getRegionSlug } from "src/utils/routing";

const MetricPage: NextPage<{ metricId: string }> = ({ metricId }) => {
const metric = metricCatalog.getMetric(metricId);
const metricFullName = metric.extendedName
? `${metric.name}: ${metric.extendedName}`
: metric.name;

// TODO(michael): Fetching timeseries could be expensive. We might want to make this optional.
const { data, error } = useDataForRegionsAndMetrics(
regions.all,
[metric],
/*includeTimeseries=*/ true
);

return (
<>
<PageMetaTags
siteName={cms.settings.siteName}
title={`Metrics: ${metric.id}`}
description={metricFullName}
url={`/internal/metrics/${metric.id}`}
/>
<Container maxWidth="md" sx={{ py: 5 }}>
{/* TODO: Implement a basic version in the packages repo */}
<Breadcrumbs aria-label="breadcrumb" sx={{ my: 2 }}>
<Link color="inherit" underline="hover" href="/">
Home
</Link>
<Link color="inherit" underline="hover" href="/internal">
Internal
</Link>
<Link color="text.primary" underline="hover" href="/internal/metrics">
Metrics
</Link>
<Link
color="text.primary"
underline="hover"
href={`/internal/metrics/${metricId}`}
>
{metricFullName}
</Link>
</Breadcrumbs>

<Typography variant="h1">{metric.id}</Typography>
<Typography>Name: {metric.name}</Typography>
<Typography>Extended name: {metric.extendedName}</Typography>
<Typography>
Categories:{" "}
{!metric.categoryValues ? "N/A" : metric.categoryValues.join(", ")}
</Typography>

<Typography variant="h2" mt={1}>
Definition
</Typography>
<pre>{JSON.stringify(metric, null, 2)}</pre>

<Typography variant="h2" mt={1}>
Data Available
</Typography>
{error && (
<Stack>
<Typography>Failed to fetch data:</Typography>
<pre>error</pre>
<Typography>
More details may be available in the console.
</Typography>
</Stack>
)}
{data ? (
<RegionDataTable data={data} metric={metric} />
) : (
<CircularProgress />
)}
</Container>
</>
);
};

const RegionDataTable = ({
data,
metric,
}: {
data: MultiRegionMultiMetricDataStore;
metric: Metric;
}) => {
return (
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>Region</TableCell>
<TableCell align="right">Value</TableCell>
<TableCell align="right">Min Date</TableCell>
<TableCell align="right">Max Date</TableCell>
<TableCell align="right">Length</TableCell>
</TableRow>
</TableHead>
<TableBody>
{regions.all.map((region) => (
<RegionDataRow
key={region.regionId}
data={data.metricData(region, metric)}
/>
))}
</TableBody>
</Table>
</TableContainer>
);
};

const RegionDataRow = ({ data }: { data: MetricData }) => {
const ts = data.timeseries;
return (
<TableRow
key={data.region.regionId}
sx={{
"&:last-child td, &:last-child th": { border: 0 },
}}
>
<TableCell component="th" scope="row">
<Link
href={`/internal/metrics/${data.metric.id}/${getRegionSlug(
data.region
)}`}
>
{data.region.fullName}
</Link>
</TableCell>
<TableCell align="right">
{data.currentValue ? data.formatValue() : "no data"}
</TableCell>
{!ts.hasData() ? (
<TableCell colSpan={3}>no timeseries</TableCell>
) : (
<>
<TableCell align="right">{isoDateOnlyString(ts.minDate)}</TableCell>
<TableCell align="right">{isoDateOnlyString(ts.maxDate)}</TableCell>
<TableCell align="right">{ts.length}</TableCell>
</>
)}
</TableRow>
);
};

interface MetricPageParams extends ParsedUrlQuery {
metricId: string;
}

export const getStaticProps: GetStaticProps = async ({ params }) => {
const { metricId } = params as MetricPageParams;
return { props: { metricId } };
};

export const getStaticPaths: GetStaticPaths = async () => {
const paths = metricCatalog.metrics.map((metric) => ({
params: { metricId: metric.id },
}));
return { paths, fallback: false };
};

export default MetricPage;
Loading