Public
Edited
Aug 8, 2023
1 fork
3 stars
Insert cell
Insert cell
Insert cell
({minTide, maxTide})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
html`${expandedShifts.map(({start, end, minTideFeet, minTideTime, people}) => {
let lowestTide = '';
if (minTide !== Infinity) {
lowestTide = `Lowest tide is ${minTideFeet}ft at ${minTideTime.toLocaleTimeString("en-US", {timeZone: "UTC"})} so `;
}
let day = start.toISOString().split("T")[0];
return `<h3>${start.toLocaleDateString("en-US", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
timeZone: "UTC"
})}</h3><p>Shift from ${start.toLocaleTimeString("en-US", {
timeZone: "UTC"
})} to ${end.toLocaleTimeString("en-US", {
timeZone: "UTC"
})} (${displayDurationInMinutes(start, end)})<br>${
predictionForDate(day).innerHTML || ""
}${lowestTide} the shift needs ${people} people</p>`;
}).join("") || "There are no shifts, try playing with the sliders"}`
Insert cell
Inputs.table(expandedShifts)
Insert cell
// These are shifts with extra information about tide height, daylight etc
expandedShifts = shifts.map(([start, end]) => {
let daylights = daylight_by_day[start.toISOString().split("T")[0]];
let {minTideFeet, minTideTime} = getMinTide(start, end);
let people = peoplePerShift;
if (minTideFeet < superLowTide) {
people = peoplePerSuperLowShift;
}
start = roundToFifteenMinutes(start);
end = roundToFifteenMinutes(end);
return {date: start.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
timeZone: "UTC"
}), duration: Math.round((end.getTime() - start.getTime()) / 60000), minTideFeet, minTideTime, people, start, end, ...daylights}
})
Insert cell
html`<h2>Copy and paste these ${expandedShifts.length} shifts</h2>
<p>Note that even though these end in 000Z they are in America/Los_Angeles timezone.</p>
<textarea style="width: 90%; height: 20em;">${JSON.stringify(expandedShifts, null, ' ')}</textarea>`
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
stationID = 9414131 // if we change the ID here we need to also change the daylight hours calculation
Insert cell
minTide = -2 // Hardcoded maximum tide used in the range picker
Insert cell
maxTide = 8; // Hardcoded minimum tide used in the range picker
Insert cell
Insert cell
Insert cell
Insert cell
daylight_by_day = Object.fromEntries(
(
await (
await fetch(
`https://www.rockybeaches.com/data.json?sql=${`select
day,
dawn,
dusk
from
sunrise_sunset
where
place = :place
order by
day`}&_shape=array&place=pillar-point`
)
).json()
).map((o) => [
o.day,
{
dawn: new Date(o.day + "T" + o.dawn + "Z"),
dusk: new Date(o.day + "T" + o.dusk + "Z")
}
])
)
Insert cell
md`## Figure out when the low tides are

See https://github.com/natbat/pillarpointstewards/issues/63 for details of the latest version of this query.`
Insert cell
sqlQuery = {
return `
with inner as (
select
datetime,
substr(datetime, 0, 11) as date,
mllw_feet,
lag(mllw_feet) over win as previous_mllw_feet,
lead(mllw_feet) over win as next_mllw_feet
from
tide_predictions
join sunrise_sunset on place = :sunlight_place
and day = date(datetime)
where
case
when strftime('%w', datetime) in ('0', '6') then mllw_feet < :tide_weekends
else mllw_feet < :tide
end
and station_id = :station_id
and datetime >= date()
and time(datetime) > dawn
and time(datetime) < dusk
window win as (
order by datetime
)
order by
datetime
),
lowest_tide_per_day as (
select
date,
datetime,
mllw_feet
from
inner
where
mllw_feet <= previous_mllw_feet
and mllw_feet <= next_mllw_feet
)
select
min(datetime) as datetime,
mllw_feet
from
lowest_tide_per_day
group by
date
order by
date`;
}
Insert cell
linkSuffix = `?station_id=9414131&tide=${weekdayTide}&tide_weekends=${weekendTide}&sunlight_place=pillar-point&sql=${encodeURIComponent(
sqlQuery
)}&_shape=array`
Insert cell
md`https://www.rockybeaches.com/data.json${linkSuffix}`
Insert cell
heights = (
  await fetch(
    `https://www.rockybeaches.com/data.json${linkSuffix}`
  )
).json()

Insert cell
Insert cell
Inputs.table(heights)
Insert cell
low_tides = heights.map((o) => new Date(o.datetime.replace(" ", "T") + ":00Z"))
Insert cell
function getMinTide(start, end) {
// Note that this can return Infinity if there are no heights
// records between start and end - which can happen if code
// elsewhere has truncated a shift due to time of day
// Returns [minTide, datetime]
let heightsBetween = heights.filter(h => {
let dt = (new Date(h.datetime.replace(" ", "T") + ":00Z"));
return (dt > start) && (dt < end);
});
heightsBetween.sort((a, b) => a.mllw_feet - b.mllw_feet);
if (heightsBetween.length == 0) {
return Infinity;
}
let minHeight = heightsBetween[0];
return {minTideFeet: minHeight.mllw_feet, minTideTime: (new Date(minHeight.datetime.replace(" ", "T") + ":00Z"))};
}
Insert cell
getMinTide(new Date("2022-04-10T13:00:00Z"), new Date("2022-05-20T13:00:00Z"))
Insert cell
Insert cell
Insert cell
// Shifts are the chunks plus shiftBufferBefore/After hours before and after
shifts = low_tides.map((dt) => {
let newStart = new Date(dt.valueOf() - shiftBufferBefore * 60 * 60 * 1000);
let newEnd = new Date(dt.valueOf() + shiftBufferAfter * 60 * 60 * 1000);
var day = newStart.toISOString().split("T")[0];
let { dawn, dusk } = daylight_by_day[day];

// add a buffer to dawn so that they dont have to start at the crack of dawn, basically making the earliest shift roughly 8am in winter and roughly 6am in summer if the buffer is 1 hour after dawn - this is defined in the user interface at the top
let pretendDawn = new Date(dawn.valueOf() + earliestShiftBuffer * 60 * 60 * 1000);
if (pretendDawn > newStart) {
newStart = pretendDawn;
}
if (dusk < newEnd) {
newEnd = dusk;
}
return [newStart, newEnd];
}).filter(([start, end]) => durationInMinutes(start, end) >= minimumShiftLengthMinutes);
Insert cell
totalShiftMinutes = {
let total = 0;
shifts.forEach(([start, end]) => {
let diff = end.getTime() - start.getTime();
let minutes = Math.round(diff / 60000);
total += minutes;
});
return total;
}

Insert cell
Insert cell
function displayDurationInMinutes(start, end) {
return minutesToHoursAndMinutes(durationInMinutes(start, end));
}
Insert cell
function durationInMinutes(start, end) {
let diff = end.getTime() - start.getTime();
return Math.round(diff / 60000);
}
Insert cell
function minutesToHoursAndMinutes(minutes) {
if (minutes > 60) {
let hours = Math.floor(minutes / 60);
let andMinutes = (minutes / 60 - hours) * 60;
let s = hours + (hours == 1 ? "hr" : "hrs");
if (andMinutes > 0) {
s += " " + Math.round(andMinutes);
s += andMinutes > 1 ? "mins" : "min";
}
return s;
} else {
return minutes + ' ' + (minutes > 1 ? "mins" : "min");
}
return minutes;
}

Insert cell
rockyBeachesHtml = (
await fetch("https://www.rockybeaches.com/us/pillar-point")
).text()
Insert cell
tidePredictions = {
let el = document.createElement("html");
el.innerHTML = rockyBeachesHtml;
Array.from(el.querySelectorAll("h4,div.depth-view")).forEach((el) =>
el.parentNode.removeChild(el)
);
return Object.fromEntries(
Array.from(el.querySelectorAll(".full .tide-prediction")).map((el) => [
el.id.replace("tide-prediction-", ""),
el
])
);
}
Insert cell
function predictionForDate(date) {
let prediction = tidePredictions[date];
if (!prediction) {
return "";
}
return html`<style>
.day-view {
position: relative;
height: 60px;
margin: 1rem 0 1rem 0;
grid-column: 4 / span 1;
}
.day-view .minima .time {
margin-top: -1rem;
color: rgba(23,106,184,0.9);
}
.day-view .minima .depth {
padding-top: 50px;
margin-top: 1.3rem;
color: rgba(23,106,184,0.9);
}
.day-view .minima span {
display: block;
width: 1px;
overflow: visible;
padding: 0;
margin-left: -1.25rem;
}
.minima {
padding-left: 3px;
border-left: 2px solid black;
display: block;
height: 100%;
position: absolute;
bottom: 0;
left: 0;
font-size: 0.75rem;
line-height: 1;
text-align: center;
}</style>
${prediction.innerHTML}
<br>`;
}
Insert cell
minShiftDuration = Math.min(...shifts.map(([start, end])=> {
let diff = end.getTime() - start.getTime();
return Math.round(diff / 60000);
}))
Insert cell
maxShiftDuration = Math.max(...shifts.map(([start, end])=> {
let diff = end.getTime() - start.getTime();
return Math.round(diff / 60000);
}))
Insert cell
function roundToFifteenMinutes(dt) {
let msIn15Mins = 1000 * 60 * 15;
return new Date(Math.round(dt.getTime() / msIn15Mins) * msIn15Mins)
}
Insert cell
roundToFifteenMinutes(new Date())
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more