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 tidal curve graphs #64

Merged
merged 13 commits into from
Jul 17, 2024
16 changes: 16 additions & 0 deletions gatsby-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,22 @@ export const createPages = async function ({
});
});

TidalData.schedule.forEach((day: TidesJson_ScheduleObject, index: number) => {
actions.createPage({
path: "/tide-graph/" + day.date,
component: path.resolve(`./src/components/templates/TideGraphPage.tsx`),
context: {
day,
nextDay:
index < TidalData.schedule.length - 1
? TidalData.schedule[index + 1].date
: false,
previousDay: index > 0 ? TidalData.schedule[index - 1].date : false,
},
defer: false,
});
});

// Legacy page
actions.createRedirect({
fromPath: "/historical-tables",
Expand Down
5 changes: 2 additions & 3 deletions src/components/navigation/DataInformation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ import React from "react";
export function DataInformation() {
return (
<>
<Text>Tide times for Porthmadog, North Wales, United Kingdom.</Text>
<Text>
Times are GMT/BST. Heights shown are heights above chart datum. Low
water times are not provided due to seasonal variations and river flows.
Tide times for Porthmadog, North Wales, United Kingdom. Times are
GMT/BST. Heights shown are heights above chart datum.
</Text>
<Text>
No warranty is provided for the accuracy of data displayed. Tidal Data
Expand Down
115 changes: 115 additions & 0 deletions src/components/templates/TideGraphPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import * as React from "react";
import { Link, type HeadFC, type PageProps } from "gatsby";
import { Badge, Box, Button, Group, Paper, rem, Text } from "@mantine/core";
import TidalData from "../../../data/tides.json";
import { SEO } from "../SEO";
import Layout from "../navigation/Layout";
import { DateTime } from "luxon";
import { TidesJson_PDFObject, TidesJson_ScheduleObject } from "../../types";
import {
IconAlertTriangleFilled,
IconArrowLeft,
IconArrowRight,
IconDownload,
IconExclamationCircle,
IconFileTypePdf,
IconHome,
} from "@tabler/icons-react";
import { TideTable } from "../tideTables/TideTable";
import { TideTableMobile } from "../tideTables/TideTableMobile";
import { DataInformation } from "../navigation/DataInformation";
import { TidalGraph } from "../tideGraph/TidalGraph";

const Page: React.FC<PageProps> = ({ pageContext }) => {
const { day, previousDay, nextDay } = pageContext as {
day: TidesJson_ScheduleObject;
previousDay: string | false;
nextDay: string | false;
};
return (
<Layout
title={
DateTime.fromSQL(day.date).toLocaleString({
day: "numeric",
month: "long",
year: "numeric",
}) + " Porthmadog Tide Graph"
}
headerButtons={
<>
{previousDay ? (
<Link to={"/tide-graph/" + previousDay}>
<Button leftSection={<IconArrowLeft size={14} />} variant="light">
{DateTime.fromSQL(previousDay).toLocaleString({
day: "numeric",
month: "short",
})}
</Button>
</Link>
) : null}
<Link to={"/"}>
<Button variant="light">
<IconHome size={14} />
</Button>
</Link>
{nextDay ? (
<Link to={"/tide-graph/" + nextDay}>
<Button
leftSection={<IconArrowRight size={14} />}
variant="light"
>
{DateTime.fromSQL(nextDay).toLocaleString({
day: "numeric",
month: "short",
})}
</Button>
</Link>
) : null}
</>
}
>
<TidalGraph date={DateTime.fromSQL(day.date).toJSDate()} />
<Paper shadow="xl" withBorder p="xl">
<Group justify="flex-start" mb="sm">
<Badge
color="red"
size="xl"
leftSection={
<IconAlertTriangleFilled
style={{ width: rem(15), height: rem(15) }}
/>
}
>
Warning
</Badge>
<Text fw={500} tt="uppercase">
Not to be used for navigation
</Text>
</Group>
<Text>
{" "}
Tide Graphs for Porthmadog are not published by authoritative sources
and should be considered highly unreliable due to seasonal river flows
and poorly understood tidal dynamics in the estuary. This graph is
produced by extrapolating from published high water times and heights.
</Text>
</Paper>
<Box p="sm">
<DataInformation />
</Box>
</Layout>
);
};

export default Page;

export const Head: HeadFC = ({ pageContext }) => {
const { day } = pageContext as { day: TidesJson_PDFObject };
const pageTitle =
DateTime.fromSQL(day.date).toLocaleString({
day: "numeric",
month: "long",
year: "numeric",
}) + " Porthmadog Tide Graph";
return <SEO title={pageTitle} />;
};
43 changes: 43 additions & 0 deletions src/components/tideGraph/TidalGraph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from "react";
import { TidesJson_ScheduleObject } from "../../types";
import TidalData from "../../../data/tides.json";
import { TidalGraphComponent } from "./TidalGraphComponent";

export function TidalGraph({ date }: { date: Date }) {
const startTimestamp = date;
startTimestamp.setHours(0, 0, 0, 0);
const endTimestamp = new Date(startTimestamp);
endTimestamp.setDate(endTimestamp.getDate() + 1); // The charts don't work so well beyond a day
let startIndex = TidalData.schedule.findIndex(
(date: TidesJson_ScheduleObject) => {
return new Date(date.date) >= startTimestamp;
}
);
let endIndex = TidalData.schedule.findIndex(
(date: TidesJson_ScheduleObject) => {
return new Date(date.date) >= endTimestamp;
}
);

// Adjust indices to include the days immediately before and after the range to capture them in the graph
startIndex = startIndex > 0 ? startIndex - 1 : startIndex;
endIndex = endIndex < TidalData.schedule.length ? endIndex + 1 : endIndex;

// Slice the array to get the desired elements
const highTides = TidalData.schedule
.slice(startIndex, endIndex)
.flatMap((date: TidesJson_ScheduleObject) =>
date.groups.map((tide) => ({
timestamp: new Date(date.date + " " + tide.time).getTime() / 1000,
height: Number(tide.height),
}))
);

return (
<TidalGraphComponent
highTides={highTides}
startTimestamp={startTimestamp.getTime() / 1000}
endTimestamp={endTimestamp.getTime() / 1000}
/>
);
}
112 changes: 112 additions & 0 deletions src/components/tideGraph/TidalGraphComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import React from "react";
import { Paper, Text } from "@mantine/core";
import { LineChart } from "@mantine/charts";
import { graphDataGenerator } from "./graphDataGenerator";

interface ChartTooltipProps {
label: string;
payload: Record<string, any>[] | undefined;
highTides: Array<{ timestamp: number; height: number }>;
}

function ChartTooltip({ label, payload, highTides }: ChartTooltipProps) {
if (!payload) return null;
const currentTide = highTides.find(
(tide) => tide.timestamp === Number(label)
);
return (
<Paper px="md" py="sm" withBorder shadow="md" radius="md">
<Text fw={500} mb={5}>
{new Date(Number(label) * 1000).toLocaleDateString("en-GB", {
day: "numeric",
month: "short",
year: "numeric",
hour: "numeric",
minute: "numeric",
})}
</Text>
{payload.map((item: any) => (
<Text key={item.name} c={item.color} fz="sm">
{item.name}: {item.value}m
</Text>
))}
{currentTide === undefined && (
<Text c="red" fz="sm">
Caution: Estimated tide height
</Text>
)}
</Paper>
);
}

export function TidalGraphComponent({
highTides,
startTimestamp,
endTimestamp,
}: {
highTides: Array<{ timestamp: number; height: number }>;
startTimestamp: number;
endTimestamp: number;
}) {
const graphData = graphDataGenerator(highTides);
return (
<LineChart
h={800}
data={graphData.filter(
(data) =>
data.date >= startTimestamp &&
data.date <= endTimestamp &&
data.Height >= 0
)}
gridAxis="none"
dataKey="date"
xAxisLabel="Date"
yAxisLabel="Height"
yAxisProps={{
domain: [0, 5.5],
allowDataOverflow: false,
interval: "equidistantPreserveStart",
tickCount: 28,
type: "number",
}}
xAxisProps={{
tickFormatter: (value: number) =>
new Date(value * 1000).toLocaleTimeString("en-GB", {
hour: "numeric",
hour12: true,
}),
padding: { left: 30, right: 30 },
interval: "equidistantPreserveStart",
allowDecimals: false,
domain: [startTimestamp, endTimestamp],
type: "number",
ticks: Array.from(Array(25).keys()).map(
(i) => startTimestamp + i * 60 * 60
),
}}
tooltipProps={{
content: ({ label, payload }) => (
<ChartTooltip label={label} payload={payload} highTides={highTides} />
),
}}
unit="m"
connectNulls={false}
series={[{ name: "Height", color: "indigo.6" }]}
curveType="natural"
dotProps={{ r: 0 }}
strokeWidth={2}
activeDotProps={{ r: 8, strokeWidth: 1, fill: "#fff" }}
referenceLines={[
{ y: 5.1, label: "MHWS", color: "red.6" },
{ y: 3.4, label: "MHWN", color: "red.6" },
//{ y: 1.8, label: "MLWN", color: "red.6" },
//{ y: 0.3, label: "MLWS", color: "red.6" },
...highTides.flatMap((tide) => ({
x: tide.timestamp,
label: tide.height + "m",
color: "gray.6",
})),
]}
/>
);
}
Loading