Repository: tarkah/tickrs (language: rust)

Issue:
Cache data between time range view changes
Hey hey :wave:

It looks like we're fetching data every time we change the time range plots.

On slow connections this leads to lag when users switch to a different time resolution and then back e.g. between 1D -> 1W -> 1D.

It looks like we already cache the data between tickets, since tab-ing through them draws the plots instantly.


Discussion / hints:
Hey, thanks for the few suggestions! Yeah, I can definitely cache the data. I'll still destroy the API connection when the time frame isn't selected, so if it's been a few minutes you've been off the 1D time frame and you switch back to it, it'll pop up immediately, but it still might take a second or two for the latest few data points to show up.

Let me know if that sounds good.
Yeap that sounds good!

Maybe we can adapt the cache based on the time range? The 5Y data can probably be cached longer than e.g. the 1D data :smile: 
So I've already got that logic built in. The 1D data has 1 minute data points so I only request an update every 1 minute, whereas the 5Y data is 1 day data points so it sleeps for 24 hours before requesting an update. However, regardless of time frame it queries the current market price every second so current price is always live and accurate.

When a stock is added, I only create an API connection for the chosen time frame instead of them all. I did this to keep things lightweight in case someone had a ton of stocks added. I'd prefer to keep it this way. 

Therefore, the cache would only be used to store data already fetched from the API so that there isn't a visual lag when switching to a time frame that's already been selected. But since the API connection needs to be recreated, any newer data points might take a second or two to show up on the graph. 

Fix patch:
diff --git a/src/event.rs b/src/event.rs
index 7a1528c..66432b7 100644
--- a/src/event.rs
+++ b/src/event.rs
@@ -314,7 +314,7 @@ pub fn handle_keys_display_options<B: Backend>(
                     .options
                     .as_mut()
                     .unwrap()
-                    .data
+                    .data()
                     .is_some()
                 {
                     app.stocks[app.current_tab]
diff --git a/src/widget/options.rs b/src/widget/options.rs
index 881d135..7ae3f79 100644
--- a/src/widget/options.rs
+++ b/src/widget/options.rs
@@ -4,6 +4,7 @@ use crate::service::{self, Service};
 
 use api::model::{OptionsData, OptionsQuote};
 use chrono::NaiveDateTime;
+use std::collections::HashMap;
 use tui::buffer::Buffer;
 use tui::layout::{Alignment, Constraint, Direction, Layout, Rect};
 use tui::style::{Color, Modifier, Style};
@@ -28,7 +29,7 @@ pub struct OptionsState {
     options_service: service::options::OptionsService,
     exp_dates: Vec<i64>,
     exp_date: Option<i64>,
-    pub data: Option<OptionsData>,
+    data: HashMap<i64, OptionsData>,
     selected_type: OptionType,
     pub selection_mode: SelectionMode,
     selected_option: Option<usize>,
@@ -43,7 +44,7 @@ impl OptionsState {
             options_service,
             exp_dates: vec![],
             exp_date: None,
-            data: None,
+            data: HashMap::new(),
             selected_type: OptionType::Call,
             selection_mode: SelectionMode::Dates,
             selected_option: None,
@@ -51,13 +52,24 @@ impl OptionsState {
         }
     }
 
+    pub fn data(&self) -> Option<&OptionsData> {
+        if let Some(date) = self.exp_date {
+            self.data.get(&date)
+        } else {
+            None
+        }
+    }
+
     fn set_exp_date(&mut self, date: i64) {
         self.exp_date = Some(date);
 
         self.options_service.set_expiration_date(date);
 
-        self.data.take();
         self.selected_option.take();
+
+        if self.data().is_some() {
+            self.set_selected_as_closest();
+        }
     }
 
     pub fn toggle_option_type(&mut self) {
@@ -71,8 +83,8 @@ impl OptionsState {
 
     fn set_selected_as_closest(&mut self) {
         let selected_range = match self.selected_type {
-            OptionType::Call => &self.data.as_ref().unwrap().calls[..],
-            OptionType::Put => &self.data.as_ref().unwrap().puts[..],
+            OptionType::Call => &self.data().as_ref().unwrap().calls[..],
+            OptionType::Put => &self.data().as_ref().unwrap().puts[..],
         };
 
         let market_price = if let Some(ref quote) = self.quote {
@@ -124,9 +136,9 @@ impl OptionsState {
     pub fn previous_option(&mut self) {
         if let Some(idx) = self.selected_option {
             let option_range = if self.selected_type == OptionType::Call {
-                &self.data.as_ref().unwrap().calls[..]
+                &self.data().as_ref().unwrap().calls[..]
             } else {
-                &self.data.as_ref().unwrap().puts[..]
+                &self.data().as_ref().unwrap().puts[..]
             };
 
             let new_idx = if idx == 0 {
@@ -142,9 +154,9 @@ impl OptionsState {
     pub fn next_option(&mut self) {
         if let Some(idx) = self.selected_option {
             let option_range = if self.selected_type == OptionType::Call {
-                &self.data.as_ref().unwrap().calls[..]
+                &self.data().as_ref().unwrap().calls[..]
             } else {
-                &self.data.as_ref().unwrap().puts[..]
+                &self.data().as_ref().unwrap().puts[..]
             };
 
             let new_idx = (idx + 1) % option_range.len();
@@ -185,7 +197,9 @@ impl OptionsState {
                         header.options[0].puts.reverse();
 
                         self.quote = Some(header.quote);
-                        self.data = Some(header.options.remove(0));
+
+                        self.data
+                            .insert(self.exp_date.unwrap(), header.options.remove(0));
 
                         if self.selected_option.is_none() {
                             self.set_selected_as_closest();
@@ -313,7 +327,7 @@ impl StatefulWidget for OptionsWidget {
             selector_chunks[1] = add_padding(selector_chunks[1], 1, PaddingDirection::Left);
             selector_chunks[1] = add_padding(selector_chunks[1], 1, PaddingDirection::Bottom);
 
-            if let Some(ref data) = state.data {
+            if let Some(data) = state.data() {
                 let selected_data = if state.selected_type == OptionType::Call {
                     &data.calls[..]
                 } else {
@@ -378,9 +392,9 @@ impl StatefulWidget for OptionsWidget {
 
             if let Some(idx) = state.selected_option {
                 let option_range = if state.selected_type == OptionType::Call {
-                    &state.data.as_ref().unwrap().calls[..]
+                    &state.data().as_ref().unwrap().calls[..]
                 } else {
-                    &state.data.as_ref().unwrap().puts[..]
+                    &state.data().as_ref().unwrap().puts[..]
                 };
 
                 if let Some(option) = option_range.get(idx) {
diff --git a/src/widget/stock.rs b/src/widget/stock.rs
index c6e5e61..cefd0d0 100644
--- a/src/widget/stock.rs
+++ b/src/widget/stock.rs
@@ -19,7 +19,7 @@ pub struct StockState {
     pub stock_service: service::stock::StockService,
     pub profile: Option<CompanyData>,
     pub current_price: f32,
-    pub prices: Vec<Price>,
+    pub prices: [Vec<Price>; 7],
     pub time_frame: TimeFrame,
     pub show_options: bool,
     pub options: Option<OptionsState>,
@@ -36,7 +36,7 @@ impl StockState {
             stock_service,
             profile: None,
             current_price: 0.0,
-            prices: vec![],
+            prices: [vec![], vec![], vec![], vec![], vec![], vec![], vec![]],
             time_frame,
             show_options: false,
             options: None,
@@ -57,11 +57,14 @@ impl StockState {
 
     pub fn set_time_frame(&mut self, time_frame: TimeFrame) {
         self.time_frame = time_frame;
-        self.prices.drain(..);
 
         self.stock_service.update_time_frame(time_frame);
     }
 
+    pub fn prices(&self) -> &[Price] {
+        &self.prices[self.time_frame.idx()]
+    }
+
     pub fn update(&mut self) {
         let updates = self.stock_service.updates();
 
@@ -71,7 +74,7 @@ impl StockState {
                     self.current_price = price;
                 }
                 service::stock::Update::Prices(prices) => {
-                    self.prices = prices;
+                    self.prices[self.time_frame.idx()] = prices;
                 }
                 service::stock::Update::CompanyData(data) => {
                     self.profile = Some(data);
@@ -91,7 +94,7 @@ impl StockState {
     }
 
     pub fn min_max(&self) -> (f32, f32) {
-        let mut data: Vec<_> = self.prices.iter().map(cast_historical_as_price).collect();
+        let mut data: Vec<_> = self.prices().iter().map(cast_historical_as_price).collect();
         data.pop();
         data.push(self.current_price);
         data = remove_zeros(data);
@@ -113,7 +116,7 @@ impl StockState {
     }
 
     pub fn high_low(&self) -> (f32, f32) {
-        let mut data = self.prices.clone();
+        let mut data = self.prices().to_vec();
 
         data.sort_by(|a, b| a.high.partial_cmp(&b.high).unwrap());
         let mut max = data.last().map(|d| d.high).unwrap_or(0.0);
@@ -136,7 +139,7 @@ impl StockState {
     pub fn x_bounds(&self) -> [f64; 2] {
         match self.time_frame {
             TimeFrame::Day1 => [0.0, 390.0],
-            _ => [0.0, (self.prices.len() + 1) as f64],
+            _ => [0.0, (self.prices().len() + 1) as f64],
         }
     }
 
@@ -153,7 +156,7 @@ impl StockState {
     }
 
     pub fn pct_change(&self) -> f32 {
-        if self.prices.is_empty() {
+        if self.prices().is_empty() {
             return 0.0;
         }
 
@@ -161,10 +164,10 @@ impl StockState {
             if let Some(profile) = &self.profile {
                 profile.price.regular_market_previous_close.price
             } else {
-                self.prices.first().map(|d| d.close).unwrap()
+                self.prices().first().map(|d| d.close).unwrap()
             }
         } else {
-            self.prices.first().map(|d| d.close).unwrap()
+            self.prices().first().map(|d| d.close).unwrap()
         };
 
         self.current_price / baseline - 1.0
@@ -290,7 +293,12 @@ impl StatefulWidget for StockWidget {
         {
             let (min, max) = state.min_max();
 
-            let mut prices: Vec<_> = state.prices.iter().map(cast_historical_as_price).collect();
+            let mut prices: Vec<_> = state
+                .prices()
+                .iter()
+                .map(cast_historical_as_price)
+                .collect();
+
             prices.pop();
             prices.push(state.current_price);
             zeros_as_pre(&mut prices);
diff --git a/src/widget/stock_summary.rs b/src/widget/stock_summary.rs
index a2021c6..e2dc071 100644
--- a/src/widget/stock_summary.rs
+++ b/src/widget/stock_summary.rs
@@ -89,7 +89,12 @@ impl StatefulWidget for StockSummaryWidget {
 
             let (min, max) = state.min_max();
 
-            let mut prices: Vec<_> = state.prices.iter().map(cast_historical_as_price).collect();
+            let mut prices: Vec<_> = state
+                .prices()
+                .iter()
+                .map(cast_historical_as_price)
+                .collect();
+
             prices.pop();
             prices.push(state.current_price);
             zeros_as_pre(&mut prices);


Test patch: