Public
Edited
May 27, 2024
Fork of Untitled
Insert cell
import { d3 } from "@observablehq/stdlib";
Insert cell
Insert cell
formatDate2 = (d) => {
const array = d.split("/");
const month = parseInt(array[1], 10); // Parse month as an integer
const day = parseInt(array[2], 10); // Parse day as an integer
return month + day / 30; // Concatenate month and day with a decimal point
}
Insert cell
parsedData = data.map(d => ({
animal_id: d.Index,
timestamp: new Date(d.Location_Timestamp_UTC),
floatTime: formatDate2(d.Location_Timestamp_UTC),
latitude: +d.Latitude,
longitude: +d.Longitude
}));
Insert cell
parsedData.forEach(item => {
item.animal_id = String(parseInt(item.animal_id));
});
Insert cell
animalIds = Array.from(new Set(parsedData.map(d => d.animal_id)));
Insert cell
Insert cell
adjustedData = parsedData.map(d => {
const adjustedLongitudes = [];
adjustedLongitudes.push(d); // Original point
if (d.longitude > -10) {
return [{ ...d, longitude: d.longitude - 360 }];
} else {
return [d];
}
return adjustedLongitudes;
}).flat();
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
map11 = {
const container = htl.html`<div style="height: 600px; position: relative; width: 900px">
<button id="playButton" style="position: absolute; bottom: 10px; left: 10px; z-index: 1000;">Play</button>
<button id="restartButton" style="position: absolute; bottom: 10px; left: 80px; z-index: 1000;">Restart</button>
<select id="birdSelect" style="position: absolute; top: 100px; left: 10px; z-index: 1000; width: 200px;">
<option value="" selected disabled>Select bird ID</option>
</select>
<div id="colorLegend" style="position: absolute; top: 150px; left: 10px; z-index: 1000;">
</div>
</div>`;

const map = L.map(container).setView([40, -190], 2); // Center the map on the equator with a global view
setTimeout(() => map.invalidateSize(true), 0);
// Add OpenStreetMap tile layer
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: "© OpenStreetMap contributors"
}).addTo(map);

let intervalId;
let isPlaying = false;
let markersLayer = L.layerGroup().addTo(map); // Layer group to hold markers and polylines
let markers = [];
let index = 0;

// Populate the dropdown menu with bird IDs
const birdSelect = container.querySelector('#birdSelect');
const uniqueBirdIds = Array.from(new Set(adjustedData.map(d => d.animal_id.trim())));
uniqueBirdIds.forEach(id => {
const option = document.createElement('option');
option.value = id;
option.textContent = id;
birdSelect.appendChild(option);
});

// Function to animate points and lines
const animateTrajectories = (filteredData, map, colorScale) => {
markersLayer.clearLayers(); // Clear previous markers and polylines
markers = []; // Reset markers array
index = 0; // Reset index

const groupedData = d3.group(filteredData, d => d.animal_id);

groupedData.forEach((values, key) => {
// Sort values by timestamp
values.sort((a, b) => a.timestamp - b.timestamp);
let latlngs = [];

values.forEach((d, i) => {
markers.push({
latlng: [d.latitude, d.longitude],
color: colorScale(d.timestamp),
popup: `Animal ID: ${d.animal_id}<br>Timestamp: ${new Date(d.timestamp).toISOString().substring(0, 10)}<br>Latitude: ${d.latitude}<br>Longitude: ${d.longitude}`,
latlngs
});
});
});

const birdIcon = L.icon({
iconUrl: northernFul.src, // 设置鸟的图标路径
iconSize: [100, 100], // 设置图标的尺寸
iconAnchor: [25, 25], // 设置图标的锚点
popupAnchor: [0, -15]
});

const step = () => {
if (index < markers.length) {
const markerData = markers[index];
markerData.latlngs.push(markerData.latlng);

// 移除之前的 bird marker
markersLayer.eachLayer(layer => {
if (layer instanceof L.Marker && layer.options.icon === birdIcon) {
markersLayer.removeLayer(layer);
}
});
const marker = L.circleMarker(markerData.latlng, {
radius: 3,
color: markerData.color,
fillOpacity: 0.5
}).addTo(markersLayer).bindPopup(markerData.popup);
if (markerData.latlngs.length > 1) {
const segment = [markerData.latlngs[markerData.latlngs.length - 2], markerData.latlng];
L.polyline(segment, { color: markerData.color }).addTo(markersLayer);
}
// 创建新的图标
const birdMarker = L.marker(markerData.latlng, {
icon: birdIcon // 使用自定义图标
}).addTo(markersLayer).bindPopup(markerData.popup);

index++;
} else {
clearInterval(intervalId);
alert("The trajectory has finished playing.");
}
};

intervalId = setInterval(step, 500); // Adjust the time interval as needed
// Add legend
const colorLegend = container.querySelector('#colorLegend');
colorLegend.innerHTML = '';
const colorScaleTicks = colorScale.ticks(10); // 设置图例的标记

colorScaleTicks.forEach((tick) => {
// 创建包含颜色方块和标签的父容器
const legendItem = document.createElement('div');
legendItem.style.display = 'flex'; // 将子元素排列在一行
legendItem.style.alignItems = 'center'; // 垂直居中对齐
// 创建颜色方块
const colorDiv = document.createElement('div');
const color = colorScale(tick);
colorDiv.style.backgroundColor = color;
colorDiv.style.width = '20px';
colorDiv.style.height = '20px';
legendItem.appendChild(colorDiv);
// 创建标签
const labelDiv = document.createElement('div');
labelDiv.textContent = new Date(tick).toISOString().substring(0, 10);
labelDiv.style.marginLeft = '5px'; // 添加一些间距
legendItem.appendChild(labelDiv);
// 将父容器添加到颜色图例和标签的父容器中
colorLegend.appendChild(legendItem);
});

};

// Event listener for play/pause button
const playButton = container.querySelector('#playButton');
playButton.addEventListener('click', () => {
if (isPlaying) {
clearInterval(intervalId);
playButton.textContent = 'Play';
} else {
if (index === 0) {
const selectedBirdId = birdSelect.value;
const filteredData = adjustedData.filter(d => d.animal_id === selectedBirdId);
if (filteredData.length > 0) {
const timeExtent = d3.extent(filteredData, d => d.timestamp);
const colorScale = d3.scaleSequential(d3.interpolateViridis).domain(timeExtent);
animateTrajectories(filteredData, map, colorScale);
// Highlight data points in chart7
const circles = d3.selectAll("circle");
circles.filter(d => d.animal_id === selectedBirdId)
.attr("fill-opacity", 1);
circles.filter(d => d.animal_id !== selectedBirdId)
.attr("fill-opacity", 0.05);
} else {
alert('No data found for the specified bird ID.');
return;
}
} else {
intervalId = setInterval(() => {
if (index < markers.length) {
const markerData = markers[index];
markerData.latlngs.push(markerData.latlng);
const marker = L.circleMarker(markerData.latlng, {
radius: 3,
color: markerData.color,
fillOpacity: 0.8
}).addTo(markersLayer).bindPopup(markerData.popup);
if (markerData.latlngs.length > 1) {
const segment = [markerData.latlngs[markerData.latlngs.length - 2], markerData.latlng];
L.polyline(segment, { color: markerData.color }).addTo(markersLayer);
}
index++;
} else {
clearInterval(intervalId);
alert("The trajectory has finished playing.");
}
}, 500); // Adjust the time interval as needed
}
playButton.textContent = 'Pause';
}
isPlaying = !isPlaying;
});
// Event listener for restart button
const restartButton = container.querySelector('#restartButton');
restartButton.addEventListener('click', () => {
clearInterval(intervalId);
playButton.textContent = 'Play';
markersLayer.clearLayers(); // Clear all markers and polylines
index = 0;
isPlaying = false;
// Reset data points opacity in chart7
const circles = d3.selectAll("circle");
circles.attr("fill-opacity", 1);
});

return container;
}

Insert cell
northernFul = FileAttachment("IMG_3303.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