diff --git a/Cargo.toml b/Cargo.toml
index e129123eb736d024167a504273dc248a54b0ae11..6a26a05776fdee570e1ffe58d58df61c6d1dc6cb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -8,16 +8,17 @@ description = "Skyward Enhanced Ground Station"
 license = "MIT"
 
 [dependencies]
-# GUI & Rendering
+# ======= GUI & Rendering =======
+egui_tiles = "0.10"
 eframe = "0.29"
 egui = { version = "0.29", features = ["log"] }
 egui_plot = "0.29"
-egui_tiles = "0.10"
-# Persistency
+# ========= Persistency =========
 serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
-# Logging
+# =========== Logging ===========
 env_logger = "0.11"
 log = "0.4"
-# Utility
+# =========== Utility ===========
+# for dynamic dispatch
 enum_dispatch = "0.3"
diff --git a/src/ui/composable_view.rs b/src/ui/composable_view.rs
index cbada0e41c34592599449c33c6c60488f667e9a6..5b308d686ffb93da6ae0d07eaec6674e33095084 100644
--- a/src/ui/composable_view.rs
+++ b/src/ui/composable_view.rs
@@ -4,10 +4,10 @@ use super::{
 };
 
 use egui::{Key, Modifiers};
-use egui_tiles::{Behavior, Container, Linear, LinearDir, Tile, Tiles, Tree};
+use egui_tiles::{Behavior, Container, Linear, LinearDir, Tile, TileId, Tiles, Tree};
 
 pub struct ComposableView {
-    tree: Tree<Pane>,
+    panes_tree: Tree<Pane>,
     behavior: ComposableBehavior,
 }
 
@@ -16,10 +16,10 @@ impl Default for ComposableView {
     fn default() -> Self {
         let mut tiles = Tiles::default();
         let root = tiles.insert_pane(Pane::default());
-        let tree = egui_tiles::Tree::new("my_tree", root, tiles);
+        let panes_tree = egui_tiles::Tree::new("my_tree", root, tiles);
 
         Self {
-            tree,
+            panes_tree,
             behavior: Default::default(),
         }
     }
@@ -31,7 +31,7 @@ impl eframe::App for ComposableView {
     fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
         // get the id of the hovered pane, in order to apply actions to it
         let hovered_pane = self
-            .tree
+            .panes_tree
             .tiles
             .iter()
             .find(|(_, tile)| matches!(tile, Tile::Pane(pane) if pane.contains_pointer()))
@@ -56,10 +56,10 @@ impl eframe::App for ComposableView {
         if let Some((action, hovered_tile)) = pane_action.take() {
             match action {
                 PaneAction::SplitH => {
-                    let hovered_tile_pane = self.tree.tiles.remove(hovered_tile).unwrap();
-                    let left_pane = self.tree.tiles.insert_new(hovered_tile_pane);
-                    let right_pane = self.tree.tiles.insert_pane(Pane::default());
-                    self.tree.tiles.insert(
+                    let hovered_tile_pane = self.panes_tree.tiles.remove(hovered_tile).unwrap();
+                    let left_pane = self.panes_tree.tiles.insert_new(hovered_tile_pane);
+                    let right_pane = self.panes_tree.tiles.insert_pane(Pane::default());
+                    self.panes_tree.tiles.insert(
                         hovered_tile,
                         Tile::Container(Container::Linear(Linear::new_binary(
                             LinearDir::Horizontal,
@@ -69,10 +69,10 @@ impl eframe::App for ComposableView {
                     );
                 }
                 PaneAction::SplitV => {
-                    let hovered_tile_pane = self.tree.tiles.remove(hovered_tile).unwrap();
-                    let replaced = self.tree.tiles.insert_new(hovered_tile_pane);
-                    let lower_pane = self.tree.tiles.insert_pane(Pane::default());
-                    self.tree.tiles.insert(
+                    let hovered_tile_pane = self.panes_tree.tiles.remove(hovered_tile).unwrap();
+                    let replaced = self.panes_tree.tiles.insert_new(hovered_tile_pane);
+                    let lower_pane = self.panes_tree.tiles.insert_pane(Pane::default());
+                    self.panes_tree.tiles.insert(
                         hovered_tile,
                         Tile::Container(Container::Linear(Linear::new_binary(
                             LinearDir::Vertical,
@@ -83,19 +83,26 @@ impl eframe::App for ComposableView {
                 }
                 PaneAction::Close => {
                     // Ignore if the root pane is the only one
-                    if self.tree.tiles.len() != 1 {
-                        self.tree.remove_recursively(hovered_tile);
+                    if self.panes_tree.tiles.len() != 1 {
+                        self.panes_tree.remove_recursively(hovered_tile);
                     }
                 }
                 PaneAction::Replace(new_pane) => {
-                    self.tree.tiles.insert(hovered_tile, Tile::Pane(*new_pane));
+                    self.panes_tree
+                        .tiles
+                        .insert(hovered_tile, Tile::Pane(*new_pane));
                 }
             }
         }
 
         // A central panel covers the remainder of the screen, i.e. whatever area is left after adding other panels.
         egui::CentralPanel::default().show(ctx, |ui| {
-            self.tree.ui(&mut self.behavior, ui);
+            self.panes_tree.ui(&mut self.behavior, ui);
+        });
+
+        // Show a panel at the bottom of the screen with few global controls
+        egui::TopBottomPanel::bottom("bottom_control").show(ctx, |ui| {
+            egui::global_theme_preference_switch(ui);
         });
     }
 }
@@ -110,21 +117,22 @@ impl Behavior<Pane> for ComposableBehavior {
     fn pane_ui(
         &mut self,
         ui: &mut egui::Ui,
-        _tile_id: egui_tiles::TileId,
+        _tile_id: TileId,
         pane: &mut Pane,
     ) -> egui_tiles::UiResponse {
         let PaneResponse {
             action_called,
             drag_response,
         } = pane.ui(ui);
+        // Capture the action and store it to be consumed in the update function
         if let Some(action_called) = action_called {
             self.action = Some(action_called);
         }
         drag_response
     }
 
-    fn tab_title_for_pane(&mut self, pane: &Pane) -> egui::WidgetText {
-        pane.tab_title()
+    fn tab_title_for_pane(&mut self, _pane: &Pane) -> egui::WidgetText {
+        "Tab".into()
     }
 }
 
diff --git a/src/ui/panes.rs b/src/ui/panes.rs
index 951d14e6c93d91973def3461a4177546829f4285..9aebb54415524b2339a11c5abafdb58cc802b5b4 100644
--- a/src/ui/panes.rs
+++ b/src/ui/panes.rs
@@ -7,24 +7,52 @@ use serde::{Deserialize, Serialize};
 
 use super::composable_view::PaneResponse;
 
-#[enum_dispatch(Pane)]
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct Pane {
+    pub pane: PaneKind,
+}
+
+impl Default for Pane {
+    fn default() -> Self {
+        Self {
+            pane: PaneKind::default(),
+        }
+    }
+}
+
+impl Pane {
+    pub fn boxed(pane: PaneKind) -> Box<Self> {
+        Box::new(Self { pane })
+    }
+}
+
+#[enum_dispatch(PaneKind)]
 pub trait PaneBehavior {
     fn ui(&mut self, ui: &mut egui::Ui) -> PaneResponse;
-    fn tab_title(&self) -> egui::WidgetText;
     fn contains_pointer(&self) -> bool;
 }
 
-// An enum to represent the different widgets available to the user.
+impl PaneBehavior for Pane {
+    fn ui(&mut self, ui: &mut egui::Ui) -> PaneResponse {
+        self.pane.ui(ui)
+    }
+
+    fn contains_pointer(&self) -> bool {
+        self.pane.contains_pointer()
+    }
+}
+
+// An enum to represent the diffent kinds of widget available to the user.
 #[derive(Clone, Debug, Serialize, Deserialize)]
 #[enum_dispatch]
-pub enum Pane {
+pub enum PaneKind {
     Default(default::DefaultPane),
     MessagesViewer(messages_viewer::MessagesViewerPane),
     Plot2D(plot_2d::Plot2DPane),
 }
 
-impl Default for Pane {
+impl Default for PaneKind {
     fn default() -> Self {
-        Pane::Default(default::DefaultPane::default())
+        PaneKind::Default(default::DefaultPane::default())
     }
 }
diff --git a/src/ui/panes/default.rs b/src/ui/panes/default.rs
index 16f5d96ab2b90cba16fd9498cfbb792111208e3c..780dfa7df8975e87d5671aac368e798167643108 100644
--- a/src/ui/panes/default.rs
+++ b/src/ui/panes/default.rs
@@ -1,4 +1,4 @@
-use super::{plot_2d::Plot2DPane, Pane, PaneBehavior};
+use super::{plot_2d::Plot2DPane, Pane, PaneBehavior, PaneKind};
 use serde::{Deserialize, Serialize};
 
 use crate::ui::composable_view::{PaneAction, PaneResponse};
@@ -45,7 +45,7 @@ impl PaneBehavior for DefaultPane {
             height_occupied += btn.rect.height();
             let btn = ui.button("Plot");
             if btn.clicked() {
-                response.set_action(PaneAction::Replace(Box::new(Pane::Plot2D(
+                response.set_action(PaneAction::Replace(Pane::boxed(PaneKind::Plot2D(
                     Plot2DPane::default(),
                 ))));
             }
@@ -75,10 +75,6 @@ impl PaneBehavior for DefaultPane {
         response
     }
 
-    fn tab_title(&self) -> egui::WidgetText {
-        "Default".into()
-    }
-
     fn contains_pointer(&self) -> bool {
         self.contains_pointer
     }
diff --git a/src/ui/panes/messages_viewer.rs b/src/ui/panes/messages_viewer.rs
index 990882e768de739339a165fa93e380ec3b41e053..cca280353f2e9b6b870d2297eeb59a5185fe178c 100644
--- a/src/ui/panes/messages_viewer.rs
+++ b/src/ui/panes/messages_viewer.rs
@@ -22,10 +22,6 @@ impl PaneBehavior for MessagesViewerPane {
         response
     }
 
-    fn tab_title(&self) -> egui::WidgetText {
-        "Messages".into()
-    }
-
     fn contains_pointer(&self) -> bool {
         self.contains_pointer
     }
diff --git a/src/ui/panes/plot_2d.rs b/src/ui/panes/plot_2d.rs
index 3f1f7c59102368901f91031edb3bd72793f9332a..a3ad8079da0f8a0c36444ed70ade6d5b06e4e680 100644
--- a/src/ui/panes/plot_2d.rs
+++ b/src/ui/panes/plot_2d.rs
@@ -5,36 +5,105 @@ use super::PaneBehavior;
 use egui_plot::{Line, PlotPoints};
 use serde::{Deserialize, Serialize};
 
-#[derive(Clone, Debug, Default, Serialize, Deserialize)]
+#[derive(Clone, Debug, Serialize, Deserialize)]
 pub struct Plot2DPane {
     #[serde(skip)]
-    pub hovered: bool,
+    pub contains_pointer: bool,
+    settings_visible: bool,
+    n_points: u32,
+    frequency: f64,
+    width: f32,
+    color: egui::Color32,
+    open: bool,
+}
+
+impl Default for Plot2DPane {
+    fn default() -> Self {
+        Self {
+            contains_pointer: false,
+            settings_visible: false,
+            n_points: 2,
+            frequency: 1.0,
+            width: 1.0,
+            color: egui::Color32::from_rgb(0, 120, 240),
+            open: false,
+        }
+    }
 }
 
 impl PaneBehavior for Plot2DPane {
     fn ui(&mut self, ui: &mut egui::Ui) -> PaneResponse {
-        let modifiers = ui.input(|i| i.modifiers);
         let mut response = PaneResponse::default();
 
+        let mut window_visible = self.settings_visible;
+        egui::Window::new("Plot Settings")
+            .id(ui.id())
+            .auto_sized()
+            .collapsible(true)
+            .movable(true)
+            .open(&mut window_visible)
+            .show(ui.ctx(), |ui| self.settings_window(ui));
+        self.settings_visible = window_visible;
+
+        let ctrl_pressed = ui.input(|i| i.modifiers.ctrl);
+
         let plot = egui_plot::Plot::new("plot");
         plot.show(ui, |plot_ui| {
-            self.hovered = plot_ui.response().contains_pointer();
-            if plot_ui.response().dragged() && modifiers.ctrl {
+            self.contains_pointer = plot_ui.response().contains_pointer();
+            if plot_ui.response().dragged() && ctrl_pressed {
                 println!("ctrl + drag");
                 response.set_drag_started();
             }
-
-            plot_ui.line(Line::new(PlotPoints::from(vec![[1.0, 0.0], [2.0, 10.0]])));
+            let points: Vec<[f64; 2]> = (0..self.n_points)
+                .map(|i| i as f64 * 100.0 / (self.n_points - 1) as f64)
+                .map(|i| [i, (i * std::f64::consts::PI * 2.0 * self.frequency).sin()])
+                .collect();
+            plot_ui.line(
+                Line::new(PlotPoints::from(points))
+                    .color(self.color)
+                    .width(self.width),
+            );
+            plot_ui.response().context_menu(|ui| self.menu(ui));
         });
 
         response
     }
 
-    fn tab_title(&self) -> egui::WidgetText {
-        "Plot".into()
+    fn contains_pointer(&self) -> bool {
+        self.contains_pointer
     }
+}
 
-    fn contains_pointer(&self) -> bool {
-        self.hovered
+impl Plot2DPane {
+    fn menu(&mut self, ui: &mut egui::Ui) {
+        ui.set_max_width(200.0); // To make sure we wrap long text
+
+        if ui.button("Settings…").clicked() {
+            self.settings_visible = true;
+            ui.close_menu();
+        }
+    }
+
+    fn settings_window(&mut self, ui: &mut egui::Ui) {
+        egui::Grid::new(ui.id())
+            .num_columns(2)
+            .spacing([10.0, 5.0])
+            .show(ui, |ui| {
+                ui.label("Size:");
+                ui.add(egui::Slider::new(&mut self.n_points, 2..=1000).text("Points"));
+                ui.end_row();
+
+                ui.label("Frequency:");
+                ui.add(egui::Slider::new(&mut self.frequency, 0.1..=10.0).text("Hz"));
+                ui.end_row();
+
+                ui.label("Color:");
+                ui.color_edit_button_srgba(&mut self.color);
+                ui.end_row();
+
+                ui.label("Width:");
+                ui.add(egui::Slider::new(&mut self.width, 0.1..=10.0).text("pt"));
+                ui.end_row();
+            });
     }
 }