Public
Edited
Apr 14
Paused
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
groupsData
Insert cell
map = {
let map = this;

if (!map) {
// let center = projection.invert([width / 2, height / 2]);

map = new mapboxgl.Map({
container,
style: r,
center: [-127, 51.8],
scrollZoom: false,
zoom: 5.3,
maxZoom: 12,
minZoom: 4
// maxBounds: bounds // Set the map's geographical boundaries.
});
}
// add navigation controls (zoom buttons, pitch & rotate)

const popup = new mapboxgl.Popup({
closeButton: false,
closeOnClick: false
});
map.on("load", function () {
map.addControl(new mapboxgl.NavigationControl());

// Keep track of which images we've already added
const addedImages = new Set();

// Handle missing images
map.on("styleimagemissing", function (e) {
console.log("Handling missing image:", e.id);
// You could load a placeholder image here if needed
});

// Process each image one at a time to avoid race conditions
const loadImages = async () => {
for (const item of groupsData) {
if (!item.file || item.group === undefined) continue;

// Convert group to string and check if we've already added it
const groupId = String(item.group);
if (addedImages.has(groupId)) {
console.log(`Image for group ${groupId} already added, skipping`);
continue;
}

try {
const iconImage = await item.file.image();
console.log(`Adding image for group: ${groupId}`);
map.addImage(groupId, iconImage);
addedImages.add(groupId);
} catch (error) {
console.error(`Error adding image for group ${groupId}:`, error);
}
}

console.log("All images processed");
// Now you can add your layers that use these images
// addLayers();
};

loadImages();
});

map.on("mouseenter", "hexbins", function (e) {
map.getCanvas().style.cursor = "default";

const coordinates = e.features[0].geometry.coordinates.slice();
const properties = e.features[0].properties;
// mutable debug = properties;
const catText =
properties.Location === undefined ? "none" : properties.Organization;

while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
}

// make the date more readable
let firstCatch = new Date(properties.firstCatch).toLocaleString(undefined, {
timeZone: "utc",
year: "numeric",
month: "numeric",
day: "numeric"
});
const dataLine = dataSentinels
.filter((d) => d.Location === properties.Location)
.sort((a, b) => a.Date - b.Date);
// mutable debug = properties;
// if (d3.sum(dataLine, (d) => d.total) > 0) {
dataLine.map((d) => {
console.log(new Date(d.Date));
});
popup
.setLngLat(coordinates)
.setHTML(
`<div>${catText} <br> Location: ${properties.Location} <br> Date of first dungeness: ${firstCatch} <br> Total over last 3 days: ${properties.total} <br> Total dungeness caught to date: ${properties.totalTODate} </div><svg class="popup-d3-svg" style="margin: 0 auto; display: block;"></svg>`
)
.addTo(map);

const width1 = 125,
height1 = 25;

const svgElement = popup
.getElement()
.getElementsByClassName("popup-d3-svg")[0];

const svg = d3
.select(svgElement)
.attr("width", width1)
.attr("height", height1);
// .style("background-color", "black");

const margin = { top: 2, right: 3, bottom: 1, left: 4 };

const x1 = d3
.scaleTime()
.domain(d3.extent(dataLine, (d) => d.Date))
.range([margin.left, width1 - margin.right]);

const y1 = d3
.scaleLinear()
.domain(d3.extent(dataLine, (d) => d.total))
.nice()
.range([height1 - margin.bottom, margin.top]);

svg
.append("path")
.datum(dataLine)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr(
"d",
d3
.line()
.x((d) => x1(d.Date))
.y((d) => y1(d.total))
);

map.on("mouseleave", "hexbins", function () {
map.getCanvas().style.cursor = "";
popup.remove();
});

map.on("click", "hexbins", function (e) {
// console.log(e);
mutable siteClicked = e.features[0].properties.Location;
container.value.selected = {
lng: e.lngLat.lng,
lat: e.lngLat.lat,
data: e.features[0].properties
};
container.dispatchEvent(new CustomEvent("input", { bubbles: true }));
});
});
// invalidation.then(() => map.remove());

// Wait until the map loads, then yield the container again.
yield new Promise((resolve) => {
if (map.loaded()) resolve(map);
else map.on("load", () => resolve(map));
});
let fc = {
type: "FeatureCollection",
features: hexgeo
};
if (map._loaded) {
if (!map.getSource("hexbins")) {
map.addSource("hexbins", {
type: "geojson",
data: fc
});
} else {
map.getSource("hexbins").setData(fc);
}
if (map.getLayer(hexbinLayer.id)) {
map.removeLayer(hexbinLayer.id);
}
// console.log("adding layer")
map.addLayer(hexbinLayer);
// map.flyTo(projection.invert([100,1000]))
// console.log("flying")
// }
return true;
}
}
Insert cell
hexgeo
Insert cell
// updateMapbox = {
// // This allows us to update the map with data without re-rendering the whole cell
// // There is a bit of weirdness around adding and removing the layer to make sure mapbox rerenders
// let fc = {
// type: "FeatureCollection",
// features: hexgeo
// };
// if (map._loaded) {
// if (!map.getSource("hexbins")) {
// map.addSource("hexbins", {
// type: "geojson",
// data: fc
// });
// } else {
// // console.log("setting source")
// map.getSource("hexbins").setData(fc);
// }
// if (map.getLayer(hexbinLayer.id)) {
// map.removeLayer(hexbinLayer.id);
// }
// // console.log("adding layer")
// map.addLayer(hexbinLayer);
// // map.flyTo(projection.invert([100,1000]))
// // console.log("flying")
// // }
// return true;
// }
// return false;
// }

Insert cell
Insert cell
hexbinLayer = {
return {
id: "hexbins",
type: "symbol",
source: "hexbins",
layout: {
"icon-allow-overlap": true,
"icon-image": ["get", "group"],
"icon-size": 0.2,
"icon-ignore-placement": true
},
paint: {
"icon-opacity": 0.82
}

// paint: {
// // keep the circles the same size
// // https://stackoverflow.com/questions/37599561/drawing-a-circle-with-the-radius-in-miles-meters-with-mapbox-gl-js
// "circle-radius": {
// stops: [
// [0, 0],
// [8.2, metersToPixelsAtMaxZoom(2, center[1])]
// ],
// base: 2
// },
// "circle-stroke-color": "blue",
// "circle-stroke-width": ["get", "stroke"],
// "circle-color": ["get", "mhwcolor"],
// "circle-opacity": ["get", "opacity"]
// }
};
}
Insert cell
clickedSite = dataSentinels.filter((d) => d.Location === siteClicked)
Insert cell
r = "mapbox://styles/hakai/cl3afkjc5001914pnjkofmxjp" //"mapbox://styles/hakai/cl3afkjc5001914pnjkofmxjp" //"mapbox://styles/hakai/ckwuxmze164c314pe8hs0s1on" //"mapbox://styles/hakai/ckwuxmze164c314pe8hs0s1on" //"mapbox://styles/hakai/ckwpei0yq08cj15nthgd4ql45" //"mapbox://styles/mapbox/dark-v10" //mapbox://styles/hakai/ckwp8ahbx2u0w15o5zwx3odtn"
Insert cell
hexgeo = hex.map((d, i) => {
let sel = {};
// if (selected && selected[0]) {
// sel = selected[0];
// }
return {
id: i,
properties: {
...d,
stroke: d.lat == sel.lat && d.lng == sel.lng ? 2 : 0,
mhwcolor:
d.collected === true ? (d.total > 0 ? "#5ec962" : "#fde725") : "grey",
opacity: 0.89
},
geometry: { type: "Point", coordinates: [d.lng, d.lat] },
type: "Feature"
};
})
Insert cell
Inputs.table(dataSentinels)
Insert cell
world = FileAttachment("countries-50m.json").json()
Insert cell
countrymesh = topojson.mesh(world, world.objects.countries, (a, b) => a !== b)
Insert cell
bc = topojson.feature(bc_coastline, bc_coastline.objects.LandLowResBC)
Insert cell
// ind = dates.indexOf(time1)
Insert cell
Insert cell
today = d3.timeDay.offset(d3.utcDay(new Date("2022-01-26")), -1) //d3.utcDay() //
Insert cell
start = d3.timeDay.offset(today, -1)
Insert cell
style = html`
<style>
.mapbox-improve-map {
display: none;
}

.brushFilter {
background-color: black;
color: "#e0f5ee"
}

.axisWhite line{
stroke: #e0f5ee;
}

.axisWhite path{
stroke: #e0f5ee;
}

// .selection{
// fill: white;
// fill-opacity: .7;
// stroke: none;
// }

.radio {

color: white;
}

</style>
`
Insert cell
datesToPlot = datesCopy
Insert cell
// colorView = "anomaly"
Insert cell
Insert cell
Insert cell
mutable siteClicked = null
Insert cell
metersToPixelsAtMaxZoom = (meters, latitude) =>
meters / 0.075 / Math.cos((latitude * Math.PI) / 180)
Insert cell
Insert cell
Insert cell
urlData = "https://docs.google.com/spreadsheets/d/e/2PACX-1vSaZgKJoGSU7-ERYWCbtjuRZQxqKZ9AyPq4nkpiX3z_jwptf3-AbwpANhssfY3xL0H_IfkyKpOcLrql/pub?gid=473834616&single=true&output=csv"
Insert cell
viewof table = Inputs.table(dataSentinels)
Insert cell
dataSentinels[0]["Number of Instars"] + 9
Insert cell
legend = {
const groupsVisible = groupsData.filter((item) => item.group !== 10);
const promises = groupsVisible.map(async (item) => ({
...item,
iconUrl: await item.file.url()
}));
const items = Promise.all(promises);
return items;
}
Insert cell
dataSentinels.filter((d) => d.Location === "Winter Cove")
Insert cell
dataSentinels = {
const data = await d3.csv(urlData, d3.autoType);
data.forEach(
(d) => {
let site = sites.find((s) => s.Location === d.Location);
if (site) {
(d.Lat = site.Latitude), (d.Long = site.Longitude);
}
d.total = d["Number of Megalopae"] + d["Number of Instars"];

d.count = d.total;
d.Date = new Date(d.Date);
}
// d.status =
);
return data.filter((d) => d["Submission ID"] > 0);
}
Insert cell
d3.csv(urlData, d3.autoType)
Insert cell
url = "https://docs.google.com/spreadsheets/d/e/2PACX-1vSaZgKJoGSU7-ERYWCbtjuRZQxqKZ9AyPq4nkpiX3z_jwptf3-AbwpANhssfY3xL0H_IfkyKpOcLrql/pub?gid=229245235&single=true&output=csv"
Insert cell
sites = d3.csv(url, d3.autoType)
Insert cell
new Date(
sites[4]["Date of first Dungeness megalopae/instars"]
).toLocaleString(undefined, {
timeZone: "utc",
year: "numeric",
month: "numeric",
day: "numeric"
})
Insert cell
// selected = hexbyLocation.get(
// map.selected ? map.selected.data.lng + "|" + map.selected.data.lat : ""
// ) || []

Insert cell
hexbyLocation = d3.group(hex, (d) => d.lng + "|" + d.lat)
Insert cell
CollectionsForLastThreeDays = dataSentinels.filter(
(d) =>
// get three day old data. There can be multiple days for a site
new Date(d.Date).toISOString().substring(0, 10) <=
new Date(time1).toISOString().substring(0, 10) &&
new Date(d.Date).toISOString().substring(0, 10) >=
new Date(d3.timeDay.offset(new Date(time1), -3))
.toISOString()
.substring(0, 10)
)
Insert cell
hex = {
const data = [];
for (let i = 0; i < sites.length; i++) {
data.push({
Location: sites[i].Location,
Organization: sites[i]["Partner Organizations"],
lat: sites[i].Latitude,
lng: sites[i].Longitude,
total: 0,
collected: false,
// group: 1,
// category: "none",
firstCatch: sites[i]["Date of first Dungeness megalopae/instars"],
totalTODate:
sites[i][
"Total number of Dungeness recorded to date (megalopae and instars)"
],
day: new Date(time1)
});
}
// replace categories with real value
if (CollectionsForLastThreeDays.length > 0) {
data.forEach((d) => {
let isData = CollectionsForLastThreeDays.filter(
(h) => h.Location === d.Location
);
// console.log(isData.length);
if (isData.length > 0) {
d.collected = true;

d.total = d3.sum(isData, (d) => d.total);
d.group = d.total > 0 ? 3 : 2;
}
});
}

return data;
}
Insert cell
dates = d3.timeDay
.range(
d3.timeDay.offset(new Date(dateExtent[0]), -1),
d3.timeDay.offset(new Date(dateExtent[1]), 1)
)
.map((d) => +d)
Insert cell
dateExtent = d3.extent(dataSentinels, (d) => new Date(d.Date)) //d3.extent(HWStatus, (d) => new Date(d.date_start1))
Insert cell
center = projection.invert([width/2, height/2])
Insert cell
// map._loaded
Insert cell
// map.getSource("hexbins")
Insert cell
originalScale = 25355.18980109889
Insert cell
scaleRatio = projection.scale() / originalScale
Insert cell
widthRatio = width / 955
Insert cell
heightRatio = height / 500
Insert cell
projection = d3
.geoAlbers()
.rotate([126, 0])
.fitSize(
[width, height],
topojson.feature(BC_Midres, BC_Midres.objects.BC_Midres_latlng)
)
Insert cell
pixelRadius = 100 * scaleRatio * d3.min([widthRatio, heightRatio])
Insert cell
hexbin = d3
.hexbin()
.extent([
[0, 0],
[width, height]
])
.radius(100)
Insert cell
numOfDates = d3.timeDay.count(...dateExtent)
Insert cell
height = 600
Insert cell
numFormat = d3.format(",d")
Insert cell
timeFormat = d3.timeFormat("%Y-%m-%d")
Insert cell
// import {Scrubber} from "@mbostock/scrubber"
Insert cell
Insert cell
import { BC_Midres } from "@mbrownshoes/how-i-start-maps-in-d3"
Insert cell
d3 = require("d3@6.0.0-rc.3", "d3-hexbin@0.2")
Insert cell
mapboxgl = {
const gl = await require("mapbox-gl");
if (!gl.accessToken) {
gl.accessToken =
"pk.eyJ1IjoiaGFrYWkiLCJhIjoiY20wbXh4emprMDc3cjJrcTI5czI3cXRjbCJ9.XNfWqelIzmfMTVRJlc7nIg";
const href = await require.resolve("mapbox-gl/dist/mapbox-gl.css");
document.head.appendChild(html`<link href=${href} rel=stylesheet>`);
}
return gl;
}
Insert cell
import { textcolor } from "@observablehq/text-color-annotations-in-markdown"
Insert cell
alldates = d3.timeMonth
.range(new Date(dateExtent[0]), new Date(dateExtent[1]))
.map((d) => d.toISOString().substring(0, 10))
Insert cell
import { SpikeMap } from "@d3/spike-map"
Insert cell
groupsData = [
{
title: "No recent data",
icon: "trap-not-checked",
group: 1,
file: FileAttachment("notchecked@1.png")
},
{
title: "Checked within past 3 days",
icon: "trap-checked",
group: 2,
file: FileAttachment("lighttrapchecked@1.png")
},
{
title: "Dungeness caught within past 3 days",
icon: "crabs-found",
group: 3,
file: FileAttachment("lightrapchecked_megalopa2 copy 2-1 (2).png")
}
]
Insert cell
movementRus21 = FileAttachment("lightrapchecked_megalopa2 copy 2-1 (2).png").image()
Insert cell
FileAttachment("lighttrapchecked@1.png").image()
Insert cell
FileAttachment("notchecked@1.png").image()
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