Notebooks 2.0 is here.

Public
Edited
Nov 8, 2024
Insert cell
Insert cell
{
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.style("background-color", "#1e1e1e") // Dark background
.style("font-family", "Roboto, sans-serif");

// Define scales for X and Y axes
const x = d3.scaleTime()
.domain([new Date("2023-01-01"), new Date("2023-01-20")])
.range([margin.left, width - margin.right]);

const y = d3.scaleBand()
.domain(["Aggressor", "Friendly 1", "Friendly 2"])
.range([margin.top, height - margin.bottom])
.padding(0.5);

// Add horizontal guide lines for each satellite row
y.domain().forEach((satellite) => {
svg.append("line")
.attr("x1", margin.left)
.attr("x2", width - margin.right)
.attr("y1", y(satellite) + y.bandwidth() / 2)
.attr("y2", y(satellite) + y.bandwidth() / 2)
.style("stroke", "#444")
.style("stroke-width", 1)
.style("stroke-dasharray", "4,2");
});

// Tooltip div element (hidden initially)
const tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("position", "absolute")
.style("background", "#333")
.style("color", "#fff")
.style("padding", "8px 12px")
.style("border-radius", "4px")
.style("font-size", "12px")
.style("pointer-events", "none")
.style("opacity", 0);

// Solar Storm Highlighted Bar with Tooltip Details and Wrapped Text Label
const storm = events.find(d => d.type === "storm");
if (storm) {
const stormStart = new Date(storm.dateStart);
const stormEnd = new Date(storm.dateEnd);

const stormRect = svg.append("rect")
.attr("x", x(stormStart))
.attr("y", margin.top)
.attr("width", x(stormEnd) - x(stormStart))
.attr("height", height - margin.top - margin.bottom)
.style("fill", "red")
.style("opacity", 0.2)
.on("mouseover", function (event) {
tooltip.transition().duration(200).style("opacity", 0.9);
tooltip.html(`
<strong>Solar Storm Alert</strong><br>
Duration: ${storm.dateStart} to ${storm.dateEnd}<br>
Forecast: High solar activity<br>
Expected Impact: Communication disruptions, increased radiation<br>
Risk Level: Severe
`)
.style("left", (event.pageX + 15) + "px")
.style("top", (event.pageY - 28) + "px");

// Highlight effect: Add green border on hover
stormRect.attr("stroke", "#00F0FF")
.attr("stroke-width", 3);
})
.on("mousemove", function (event) {
tooltip.style("left", (event.pageX + 15) + "px")
.style("top", (event.pageY - 28) + "px");
})
.on("mouseout", function () {
tooltip.transition().duration(200).style("opacity", 0);

// Remove highlight effect on mouseout
stormRect.attr("stroke", "none");
});

// Wrapped text for the storm label
const stormLabel = ["Solar Storm:", "Severe Impact", "Expected"];
stormLabel.forEach((line, i) => {
svg.append("text")
.attr("x", x(stormStart) + 5)
.attr("y", margin.top + 20 + i * 15)
.attr("fill", "grey")
.style("font-size", "14px")
.style("font-weight", "light")
.text(line);
});
}

// Add the X-axis with dark theme styling
svg.append("g")
.attr("transform", `translate(0, ${height - margin.bottom})`)
.call(d3.axisBottom(x).ticks(10).tickFormat(d3.timeFormat("%b %d")))
.style("color", "#aaa")
.style("font-family", "Roboto, sans-serif")
.selectAll("path, line")
.style("stroke", "#aaa");

// Add the Y-axis with dark theme styling
svg.append("g")
.attr("transform", `translate(${margin.left}, 0)`)
.call(d3.axisLeft(y))
.style("color", "#aaa")
.style("font-family", "Roboto, sans-serif")
.selectAll("path, line")
.style("stroke", "#aaa");

// Plot Maneuvers and Proximity Alerts with hover tooltips
events.forEach(d => {
const yPos = y(d.sat);

if (d.type === "maneuver") {
const color = d.sat === "Aggressor" ? "#ff6666" : "#66b3ff";
svg.append("line")
.attr("x1", x(new Date(d.date)) - (d.direction === "right" ? 5 : 0))
.attr("x2", x(new Date(d.date)) + (d.direction === "left" ? 5 : 0))
.attr("y1", yPos + (d.direction === "up" ? y.bandwidth() / 4 : y.bandwidth() / 2))
.attr("y2", yPos + (d.direction === "down" ? 3 * y.bandwidth() / 4 : y.bandwidth() / 2))
.attr("stroke", color)
.attr("stroke-width", 2)
.on("mouseover", function (event) {
tooltip.transition().duration(200).style("opacity", 0.9);
tooltip.html(`Maneuver: ${d.label}<br>Date: ${d.date}`)
.style("left", (event.pageX + 15) + "px")
.style("top", (event.pageY - 28) + "px");
})
.on("mousemove", function (event) {
tooltip.style("left", (event.pageX + 15) + "px")
.style("top", (event.pageY - 28) + "px");
})
.on("mouseout", function () {
tooltip.transition().duration(200).style("opacity", 0);
});
}
// Proximity Alert Events as Bubbles with hover tooltips
else if (d.type === "proximity") {
svg.append("circle")
.attr("cx", x(new Date(d.date)))
.attr("cy", yPos + y.bandwidth() / 2)
.attr("r", d.risk * 10)
.style("fill", "#ffcc66")
.style("opacity", 0.7)
.on("mouseover", function (event) {
tooltip.transition().duration(200).style("opacity", 0.9);
tooltip.html(`Proximity Alert<br>Risk: ${d.risk}<br>Date: ${d.date}`)
.style("left", (event.pageX + 15) + "px")
.style("top", (event.pageY - 28) + "px");
})
.on("mousemove", function (event) {
tooltip.style("left", (event.pageX + 15) + "px")
.style("top", (event.pageY - 28) + "px");
})
.on("mouseout", function () {
tooltip.transition().duration(200).style("opacity", 0);
});
}
});

// Plot Maneuvers and Proximity Alerts with larger hover targets and green border effect
events.forEach(d => {
const yPos = y(d.sat);

if (d.type === "maneuver") {
const color = d.sat === "Aggressor" ? "#ff6666" : "#66b3ff";
// Create the actual maneuver line
const line = svg.append("line")
.attr("x1", x(new Date(d.date)) - (d.direction === "right" ? 5 : 0))
.attr("x2", x(new Date(d.date)) + (d.direction === "left" ? 5 : 0))
.attr("y1", yPos + (d.direction === "up" ? y.bandwidth() / 4 : y.bandwidth() / 2))
.attr("y2", yPos + (d.direction === "down" ? 3 * y.bandwidth() / 4 : y.bandwidth() / 2))
.attr("stroke", color)
.attr("stroke-width", 2);

// Create a larger invisible hover target for the line
svg.append("line")
.attr("x1", line.attr("x1"))
.attr("x2", line.attr("x2"))
.attr("y1", line.attr("y1"))
.attr("y2", line.attr("y2"))
.attr("stroke", "transparent")
.attr("stroke-width", 20) // Increase width for hover target
.on("mouseover", function (event) {
tooltip.transition().duration(200).style("opacity", 0.9);
tooltip.html(`Maneuver: ${d.label}<br>Date: ${d.date}`)
.style("left", (event.pageX + 15) + "px")
.style("top", (event.pageY - 28) + "px");

line.attr("stroke", "#00F0FF").attr("stroke-width", 4);
})
.on("mousemove", function (event) {
tooltip.style("left", (event.pageX + 15) + "px")
.style("top", (event.pageY - 28) + "px");
})
.on("mouseout", function () {
tooltip.transition().duration(200).style("opacity", 0);
line.attr("stroke", color).attr("stroke-width", 2);
});
}
else if (d.type === "proximity") {
const circle = svg.append("circle")
.attr("cx", x(new Date(d.date)))
.attr("cy", yPos + y.bandwidth() / 2)
.attr("r", d.risk * 10)
.style("fill", "#ffcc66")
.style("opacity", 0.7);

// Add larger invisible hover target for the circle
svg.append("circle")
.attr("cx", circle.attr("cx"))
.attr("cy", circle.attr("cy"))
.attr("r", parseFloat(circle.attr("r")) + 20) // Increase radius for hover target
.attr("fill", "transparent")
.on("mouseover", function (event) {
tooltip.transition().duration(200).style("opacity", 0.9);
tooltip.html(`Proximity Alert<br>Risk: ${d.risk}<br>Date: ${d.date}`)
.style("left", (event.pageX + 15) + "px")
.style("top", (event.pageY - 28) + "px");

circle.attr("stroke", "#00F0FF").attr("stroke-width", 2);
})
.on("mousemove", function (event) {
tooltip.style("left", (event.pageX + 15) + "px")
.style("top", (event.pageY - 28) + "px");
})
.on("mouseout", function () {
tooltip.transition().duration(200).style("opacity", 0);
circle.attr("stroke", "none");
});
}
});


// Options menu icon for toggling visibility of legend and event list
const optionsButton = svg.append("text")
.attr("x", width - margin.right - 40)
.attr("y", margin.top + 20)
.attr("fill", "#fff")
.attr("font-size", "24px")
.style("cursor", "pointer")
.text("⋮")
.on("click", function() {
optionsMenu.style("display", optionsMenu.style("display") === "none" ? "block" : "none");
});

// Dropdown menu for toggling legend and event list
const optionsMenu = svg.append("g")
.attr("class", "options-menu")
.attr("transform", `translate(${width - margin.right - 120}, ${margin.top + 30})`)
.style("display", "none");

optionsMenu.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", 100)
.attr("height", 60)
.attr("fill", "#333")
.attr("rx", 5)
.attr("ry", 5);

optionsMenu.append("text")
.attr("x", 10)
.attr("y", 20)
.attr("fill", "#fff")
.attr("font-size", "12px")
.style("cursor", "pointer")
.text("Toggle Legend")
.on("click", function() {
const isVisible = legendGroup.style("display") === "none";
legendGroup.style("display", isVisible ? "block" : "none");
optionsMenu.style("display", "none");
});

optionsMenu.append("text")
.attr("x", 10)
.attr("y", 45)
.attr("fill", "#fff")
.attr("font-size", "12px")
.style("cursor", "pointer")
.text("Toggle Event List")
.on("click", function() {
const isVisible = eventListGroup.style("display") === "none";
eventListGroup.style("display", isVisible ? "block" : "none");
optionsMenu.style("display", "none");
});

// Draggable legend group
const legendGroup = svg.append("g")
.attr("class", "legend")
.attr("transform", `translate(${width - margin.right - 150}, ${margin.top + 70})`)
.style("display", "block")
.call(d3.drag().on("drag", function (event) {
d3.select(this).attr("transform", `translate(${event.x}, ${event.y})`);
}));

legendGroup.append("rect")
.attr("x", -10)
.attr("y", -10)
.attr("width", 140)
.attr("height", 100)
.attr("fill", "#333")
.attr("rx", 5)
.attr("ry", 5);

const legendData = [
{ color: "#ff6666", label: "Aggressor Maneuver" },
{ color: "#66b3ff", label: "Friendly Maneuver" },
{ color: "#ffcc66", label: "Proximity Alert" },
{ color: "red", label: "Solar Storm" }
];

legendData.forEach((d, i) => {
legendGroup.append("circle")
.attr("cx", 0)
.attr("cy", i * 20)
.attr("r", 6)
.style("fill", d.color);

legendGroup.append("text")
.attr("x", 15)
.attr("y", i * 20 + 4)
.style("fill", "#fff")
.style("font-size", "12px")
.text(d.label);
});

// Title and dimensions for the event list
const eventListWidth = 250;
const eventListHeight = 200;
const scrollBarWidth = 10;

// Draggable event list group with scrollable content and title
const eventListGroup = svg.append("g")
.attr("class", "event-list")
.attr("transform", `translate(${width - margin.right - eventListWidth - 10}, ${margin.top + 200})`)
.style("display", "none")
.call(d3.drag().on("drag", function (event) {
d3.select(this).attr("transform", `translate(${event.x}, ${event.y})`);
}));

// Background for event list box
eventListGroup.append("rect")
.attr("x", -10)
.attr("y", -10)
.attr("width", eventListWidth)
.attr("height", eventListHeight)
.attr("fill", "#333")
.attr("rx", 5)
.attr("ry", 5);

// Title for event list
eventListGroup.append("text")
.attr("x", 10)
.attr("y", -20)
.attr("fill", "#fff")
.attr("font-size", "14px")
.style("font-weight", "bold")
.text("Event List");

// Clipping path for event items
eventListGroup.append("defs").append("clipPath")
.attr("id", "clip-event-list")
.append("rect")
.attr("x", -10)
.attr("y", -10)
.attr("width", eventListWidth - scrollBarWidth - 5) // Adjust width for scroll bar
.attr("height", eventListHeight);

const eventListContainer = eventListGroup.append("g")
.attr("clip-path", "url(#clip-event-list)");

// Populate event items
const eventItems = eventListContainer.selectAll(".event-item")
.data(events)
.enter()
.append("text")
.attr("x", 0)
.attr("y", (d, i) => i * 20)
.attr("fill", "#fff")
.attr("font-size", "12px")
.text(d => `Date: ${d.date || d.dateStart} | Type: ${d.type}`);

// Scroll Bar Background
eventListGroup.append("rect")
.attr("x", eventListWidth - scrollBarWidth - 10)
.attr("y", -10)
.attr("width", scrollBarWidth)
.attr("height", eventListHeight)
.attr("fill", "#555");

// Scroll Thumb for dragging
const scrollThumb = eventListGroup.append("rect")
.attr("x", eventListWidth - scrollBarWidth - 10)
.attr("y", -10)
.attr("width", scrollBarWidth)
.attr("height", 30) // Adjust this height as needed based on content
.attr("fill", "#888")
.attr("rx", 5)
.attr("ry", 5)
.call(d3.drag().on("drag", function (event) {
let y = Math.max(-10, Math.min(event.y, eventListHeight - 40));
d3.select(this).attr("y", y);

// Calculate scroll offset based on thumb position
const scrollRatio = (y + 10) / (eventListHeight - 40);
const maxScroll = Math.max(0, events.length * 20 - eventListHeight);
const scrollOffset = scrollRatio * maxScroll;

eventItems.attr("y", (d, i) => i * 20 - scrollOffset);
}));





return svg.node();
}

Insert cell

events = [
// Aggressor Satellite
{ sat: "Aggressor", type: "maneuver", direction: "up", date: "2023-01-02", label: "Upward Maneuver" },
{ sat: "Aggressor", type: "maneuver", direction: "right", date: "2023-01-04", label: "Lateral Shift Right" },
{ sat: "Aggressor", type: "maneuver", direction: "down", date: "2023-01-07", label: "Altitude Decrease" },
{ sat: "Aggressor", type: "maneuver", direction: "left", date: "2023-01-09", label: "Lateral Shift Left" },
{ sat: "Aggressor", type: "maneuver", direction: "up", date: "2023-01-11", label: "Upward Maneuver" },
{ sat: "Aggressor", type: "maneuver", direction: "right", date: "2023-01-13", label: "Lateral Shift Right" },
{ sat: "Aggressor", type: "proximity", risk: 0.9, date: "2023-01-14", label: "High-Risk Proximity Alert" },
{ sat: "Aggressor", type: "maneuver", direction: "down", date: "2023-01-16", label: "Altitude Decrease" },
{ sat: "Aggressor", type: "maneuver", direction: "left", date: "2023-01-18", label: "Lateral Shift Left" },
{ sat: "Aggressor", type: "proximity", risk: 0.7, date: "2023-01-19", label: "Medium-Risk Proximity Alert" },
// Friendly Satellite 1
{ sat: "Friendly 1", type: "maneuver", direction: "down", date: "2023-01-03", label: "Altitude Decrease" },
{ sat: "Friendly 1", type: "maneuver", direction: "left", date: "2023-01-05", label: "Lateral Shift Left" },
{ sat: "Friendly 1", type: "maneuver", direction: "up", date: "2023-01-06", label: "Upward Maneuver" },
{ sat: "Friendly 1", type: "maneuver", direction: "right", date: "2023-01-08", label: "Lateral Shift Right" },
{ sat: "Friendly 1", type: "maneuver", direction: "down", date: "2023-01-10", label: "Altitude Decrease" },
{ sat: "Friendly 1", type: "maneuver", direction: "left", date: "2023-01-12", label: "Lateral Shift Left" },
{ sat: "Friendly 1", type: "proximity", risk: 0.6, date: "2023-01-13", label: "Medium-Risk Proximity Alert" },
{ sat: "Friendly 1", type: "maneuver", direction: "up", date: "2023-01-15", label: "Upward Maneuver" },
{ sat: "Friendly 1", type: "maneuver", direction: "right", date: "2023-01-17", label: "Lateral Shift Right" },
{ sat: "Friendly 1", type: "proximity", risk: 0.8, date: "2023-01-18", label: "High-Risk Proximity Alert" },

// Friendly Satellite 2
{ sat: "Friendly 2", type: "maneuver", direction: "up", date: "2023-01-04", label: "Upward Maneuver" },
{ sat: "Friendly 2", type: "maneuver", direction: "down", date: "2023-01-06", label: "Altitude Decrease" },
{ sat: "Friendly 2", type: "maneuver", direction: "left", date: "2023-01-07", label: "Lateral Shift Left" },
{ sat: "Friendly 2", type: "maneuver", direction: "right", date: "2023-01-09", label: "Lateral Shift Right" },
{ sat: "Friendly 2", type: "proximity", risk: 0.3, date: "2023-01-10", label: "Low-Risk Proximity Alert" },
{ sat: "Friendly 2", type: "maneuver", direction: "up", date: "2023-01-12", label: "Upward Maneuver" },
{ sat: "Friendly 2", type: "maneuver", direction: "down", date: "2023-01-14", label: "Altitude Decrease" },
{ sat: "Friendly 2", type: "maneuver", direction: "left", date: "2023-01-15", label: "Lateral Shift Left" },
{ sat: "Friendly 2", type: "maneuver", direction: "right", date: "2023-01-17", label: "Lateral Shift Right" },
{ sat: "Friendly 2", type: "proximity", risk: 0.5, date: "2023-01-18", label: "Medium-Risk Proximity Alert" },

// Solar Storm Event affecting all satellites
{ type: "storm", dateStart: "2023-01-09", dateEnd: "2023-01-11", label: "Solar Storm" }
];

Insert cell
d3 = require("d3@6")

Insert cell
margin = ({
top: 20,
right: 30,
bottom: 30,
left: 100
})
Insert cell
height = 450

Insert cell
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