Skip to content

Commit 6849018

Browse files
brainlessclaude
andcommitted
Add time range filter buttons to historical data graph
Implemented time range selection buttons (5D, 1M, 3M, 6M, 1Y, 5Y, All) for the stock historical data view. The buttons are positioned on the same line as the Back button, aligned to the right side. Changes: - Add TimeRange enum with conversion methods to days and display labels - Add selected_time_range field to track current filter (defaults to 3M) - Update load_plot_data to respect selected time range - Add change_time_range method to switch filters and reload data - Implement plot_needs_reset flag to fully reset graph view on mode change - Add UI buttons with visual highlight for selected range - Use Plot.reset() to clear zoom/pan state when switching ranges The graph now completely resets (axes, data, zoom, pan) when: - Clicking any time range button - Switching between stocks - Returning to a previously selected time range 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 53c3b78 commit 6849018

2 files changed

Lines changed: 109 additions & 10 deletions

File tree

indistocks-gui/src/app.rs

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,43 @@ pub enum View {
1313
Logs,
1414
}
1515

16+
#[derive(Debug, Clone, Copy, PartialEq)]
17+
pub enum TimeRange {
18+
FiveDays,
19+
OneMonth,
20+
ThreeMonths,
21+
SixMonths,
22+
OneYear,
23+
FiveYears,
24+
All,
25+
}
26+
27+
impl TimeRange {
28+
pub fn to_days(&self) -> Option<i64> {
29+
match self {
30+
TimeRange::FiveDays => Some(5),
31+
TimeRange::OneMonth => Some(30),
32+
TimeRange::ThreeMonths => Some(90),
33+
TimeRange::SixMonths => Some(180),
34+
TimeRange::OneYear => Some(365),
35+
TimeRange::FiveYears => Some(365 * 5),
36+
TimeRange::All => None, // None means load all data
37+
}
38+
}
39+
40+
pub fn label(&self) -> &str {
41+
match self {
42+
TimeRange::FiveDays => "5D",
43+
TimeRange::OneMonth => "1M",
44+
TimeRange::ThreeMonths => "3M",
45+
TimeRange::SixMonths => "6M",
46+
TimeRange::OneYear => "1Y",
47+
TimeRange::FiveYears => "5Y",
48+
TimeRange::All => "All",
49+
}
50+
}
51+
}
52+
1653
pub struct IndistocksApp {
1754
pub current_view: View,
1855
pub db_conn: Arc<Mutex<Connection>>,
@@ -35,6 +72,8 @@ pub struct IndistocksApp {
3572
pub plot_loaded_range: Option<(NaiveDate, NaiveDate)>, // Track what data is currently loaded
3673
pub plot_earliest_available: Option<NaiveDate>, // Earliest date available in DB for current symbol
3774
pub plot_loading_in_progress: bool, // Prevent concurrent loads
75+
pub selected_time_range: TimeRange, // Current time range filter for the plot
76+
pub plot_needs_reset: bool, // Flag to reset plot view on next render
3877
// Search caching
3978
pub last_search_query: String,
4079
pub search_results: Vec<String>,
@@ -86,6 +125,8 @@ impl IndistocksApp {
86125
plot_loaded_range: None,
87126
plot_earliest_available: None,
88127
plot_loading_in_progress: false,
128+
selected_time_range: TimeRange::ThreeMonths, // Default to 3 months
129+
plot_needs_reset: false,
89130
last_search_query: String::new(),
90131
search_results: Vec::new(),
91132
stocks_price_from: String::new(),
@@ -127,6 +168,7 @@ impl IndistocksApp {
127168
self.plot_loaded_range = None;
128169
self.plot_earliest_available = None;
129170
self.plot_loading_in_progress = false;
171+
self.plot_needs_reset = true; // Reset plot view when loading new stock
130172

131173
let conn = self.db_conn.lock().unwrap();
132174

@@ -165,9 +207,14 @@ impl IndistocksApp {
165207
println!("Data available from {} to {} ({} days span, {} data points in DB)",
166208
earliest, latest, (latest - earliest).num_days(), total_count);
167209

168-
// Load last 3 months of data initially
169-
let start = latest - chrono::Duration::days(90);
170-
let load_from = if start < earliest { earliest } else { start };
210+
// Load data based on selected time range
211+
let load_from = match self.selected_time_range.to_days() {
212+
Some(days) => {
213+
let start = latest - chrono::Duration::days(days);
214+
if start < earliest { earliest } else { start }
215+
},
216+
None => earliest, // Load all data
217+
};
171218

172219
match get_stock_data_in_range(&conn, symbol, load_from, latest) {
173220
Ok(data) => {
@@ -242,6 +289,22 @@ impl IndistocksApp {
242289
self.plot_loading_in_progress = false;
243290
}
244291
}
292+
293+
/// Change the time range and reload data for the current symbol
294+
pub fn change_time_range(&mut self, time_range: TimeRange) {
295+
self.selected_time_range = time_range;
296+
297+
// Reset plot state to ensure graph resets
298+
self.plot_data.clear();
299+
self.plot_loaded_range = None;
300+
self.plot_loading_in_progress = false;
301+
self.plot_needs_reset = true; // Reset plot view when changing time range
302+
303+
// Reload the data if a symbol is selected
304+
if let Some(symbol) = &self.selected_symbol.clone() {
305+
self.load_plot_data(symbol);
306+
}
307+
}
245308
}
246309

247310
impl eframe::App for IndistocksApp {

indistocks-gui/src/ui/main_content.rs

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::app::IndistocksApp;
1+
use crate::app::{IndistocksApp, TimeRange};
22
use chrono::{Datelike, Duration, NaiveDate};
33

44

@@ -23,8 +23,8 @@ pub fn render(ui: &mut egui::Ui, app: &mut IndistocksApp) {
2323
let (x_fmt, should_filter_ticks) = get_date_format_and_filter(days_diff);
2424
let x_fmt_clone = x_fmt.clone();
2525

26-
// Plot the data - use symbol in ID to reset view when switching stocks
27-
let plot = egui_plot::Plot::new(format!("price_plot_{}", symbol))
26+
// Plot the data - use symbol and time range in ID to reset view when switching stocks or time ranges
27+
let mut plot = egui_plot::Plot::new(format!("price_plot_{}_{}", symbol, app.selected_time_range.label()))
2828
.height(600.0)
2929
.legend(egui_plot::Legend::default())
3030
.allow_zoom([true, false]) // Allow horizontal zoom only
@@ -39,6 +39,12 @@ pub fn render(ui: &mut egui::Ui, app: &mut IndistocksApp) {
3939
value.y)
4040
});
4141

42+
// Reset plot view if needed (when changing time range or loading new stock)
43+
if app.plot_needs_reset {
44+
plot = plot.reset();
45+
app.plot_needs_reset = false;
46+
}
47+
4248
let response = plot.show(ui, |plot_ui| {
4349
let points: egui_plot::PlotPoints = app.plot_data
4450
.iter()
@@ -98,10 +104,40 @@ pub fn render(ui: &mut egui::Ui, app: &mut IndistocksApp) {
98104
}
99105
}
100106

101-
if ui.button("Back").clicked() {
102-
app.selected_symbol = None;
103-
app.plot_data.clear();
104-
}
107+
// Horizontal layout for Back button and time range buttons
108+
ui.horizontal(|ui| {
109+
if ui.button("Back").clicked() {
110+
app.selected_symbol = None;
111+
app.plot_data.clear();
112+
}
113+
114+
// Add spacing to push time range buttons to the right
115+
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
116+
// Time range buttons (in reverse order because of right_to_left layout)
117+
let time_ranges = [
118+
TimeRange::All,
119+
TimeRange::FiveYears,
120+
TimeRange::OneYear,
121+
TimeRange::SixMonths,
122+
TimeRange::ThreeMonths,
123+
TimeRange::OneMonth,
124+
TimeRange::FiveDays,
125+
];
126+
127+
for time_range in time_ranges.iter().rev() {
128+
let is_selected = app.selected_time_range == *time_range;
129+
let button = if is_selected {
130+
egui::Button::new(time_range.label()).fill(ui.style().visuals.selection.bg_fill)
131+
} else {
132+
egui::Button::new(time_range.label())
133+
};
134+
135+
if ui.add(button).clicked() {
136+
app.change_time_range(*time_range);
137+
}
138+
}
139+
});
140+
});
105141
} else if !app.search_query.is_empty() {
106142
// Show search results
107143
ui.heading("Search Results");

0 commit comments

Comments
 (0)