Notebooks 2.0 is here.
Read the preview announcement
Platform
Resources
Pricing
Sign in
Get started
Timeline Widget
Workspace
Fork
Public
By
WK
Edited
Nov 8, 2024
Insert cell
1
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.
Try it for free
Learn more
Fork
View
Export
Edit
Show 1 comment
Select
Duplicate
Copy link
Embed
Delete
JavaScript
Markdown
HTML
Add comment
Select
Duplicate
Copy link
Embed
Delete
JavaScript
Markdown
HTML
events
Add comment
Copy import
Select
Duplicate
Copy link
Embed
Delete
JavaScript
Markdown
HTML
d3
Add comment
Copy import
Select
Duplicate
Copy link
Embed
Delete
JavaScript
Markdown
HTML
margin
Add comment
Copy import
Select
Duplicate
Copy link
Embed
Delete
JavaScript
Markdown
HTML
height
Add comment
Copy import
Select
Duplicate
Copy link
Embed
Delete
JavaScript
Markdown
HTML
Edit
Add comment
Select
Duplicate
Copy link
Embed
Delete
JavaScript
Markdown
HTML