algorembrant's picture
Upload 37 files
e15ab27 verified
// ============================================================================
// tab_time.rs -- Time-Based Chart Tab
// ============================================================================
// Groups ticks by a custom time interval and renders GMP + Footprint
// profiles as horizontal bar histograms along a time x-axis.
// ============================================================================
use crate::app::AppState;
use crate::gmp_engine::{self, IntervalProfile};
use crate::timeframe;
use egui;
use egui_plot::{self, Plot, PlotPoints, Line, PlotPoint, Text};
/// Render the Time-Based tab UI and chart.
pub fn render(ui: &mut egui::Ui, state: &mut AppState) {
// ── Interval Input ──
ui.horizontal(|ui| {
ui.label("Interval:");
let resp = ui.add(
egui::TextEdit::singleline(&mut state.time_input_str)
.desired_width(80.0)
.hint_text("e.g. 1:30, 4h30m"),
);
if resp.lost_focus() || resp.changed() {
match timeframe::parse_timeframe(&state.time_input_str) {
Ok(secs) => {
state.time_interval_secs = secs;
state.profiles_dirty = true;
state.time_parse_err = None;
}
Err(e) => {
state.time_parse_err = Some(e);
}
}
}
if state.time_interval_secs > 0 {
ui.label(format!(
"= {}",
timeframe::format_seconds(state.time_interval_secs)
));
}
if let Some(ref e) = state.time_parse_err {
ui.colored_label(egui::Color32::from_rgb(255, 100, 100), e);
}
});
ui.separator();
// ── Recompute profiles if dirty ──
if state.profiles_dirty || state.time_profiles.is_empty() {
state.time_profiles = gmp_engine::aggregate_by_time(
&state.raw_ticks,
state.time_interval_secs,
state.bin_size,
);
state.profiles_dirty = false;
}
// ── Render Mode Toggle ──
ui.horizontal(|ui| {
ui.label("View:");
ui.selectable_value(&mut state.show_footprint, false, "GMP");
ui.selectable_value(&mut state.show_footprint, true, "Footprint");
ui.label(format!(
"| {} intervals | {} ticks",
state.time_profiles.len(),
state.raw_ticks.len()
));
});
ui.separator();
// ── Chart ──
render_profile_chart(ui, &state.time_profiles, state.show_footprint, state.bin_size, "time_chart");
}
/// Render a series of interval profiles as a horizontal-bar chart.
///
/// Each interval occupies one column along the x-axis.
/// Within each column, horizontal bars extend from the column center,
/// with length proportional to the stack count.
pub fn render_profile_chart(
ui: &mut egui::Ui,
profiles: &[IntervalProfile],
show_footprint: bool,
beta: f64,
plot_id: &str,
) {
if profiles.is_empty() {
ui.label("No data to display");
return;
}
let plot = Plot::new(plot_id)
.legend(egui_plot::Legend::default())
.allow_boxed_zoom(true)
.allow_drag(true)
.allow_scroll(true)
.allow_zoom(true)
.y_axis_formatter(move |y, _range, _width| {
// Show price at bin boundaries
let bin = (y.value / beta).round() as i64;
format!("{:.2}", bin as f64 * beta)
});
plot.show(ui, |plot_ui| {
// Determine global min/max bin for y-axis range
let (global_min, global_max) = profiles.iter().fold(
(i64::MAX, i64::MIN),
|(gmin, gmax), p| {
let pmin = if show_footprint { p.footprint.min_bin } else { p.gmp.min_bin };
let pmax = if show_footprint { p.footprint.max_bin } else { p.gmp.max_bin };
(gmin.min(pmin), gmax.max(pmax))
},
);
// Find max count for normalization (bar width scaling)
let max_count = profiles.iter().flat_map(|p| {
if show_footprint {
p.footprint.bins.values().map(|b| b.count).collect::<Vec<_>>()
} else {
p.gmp.bins.values().map(|b| b.count).collect::<Vec<_>>()
}
}).max().unwrap_or(1).max(1) as f64;
// Column width in x-units
let col_width = 0.8;
let bar_scale = col_width / max_count;
for (col, profile) in profiles.iter().enumerate() {
let x_center = col as f64;
if show_footprint {
// Draw footprint bars: green (up) going right, red (down) going left
for (&bin_idx, bin) in &profile.footprint.bins {
let y = bin_idx as f64 * beta + beta * 0.5; // mid-bin price
if bin.up > 0 {
let w = bin.up as f64 * bar_scale;
let points = PlotPoints::new(vec![
[x_center, y],
[x_center + w, y],
]);
plot_ui.line(
Line::new(points)
.color(egui::Color32::from_rgb(50, 200, 100))
.width(2.0),
);
}
if bin.down > 0 {
let w = bin.down as f64 * bar_scale;
let points = PlotPoints::new(vec![
[x_center, y],
[x_center - w, y],
]);
plot_ui.line(
Line::new(points)
.color(egui::Color32::from_rgb(255, 80, 80))
.width(2.0),
);
}
// Delta annotation
let delta = bin.delta();
if delta != 0 {
let label = if delta > 0 {
format!("+{}", delta)
} else {
format!("{}", delta)
};
plot_ui.text(
Text::new(PlotPoint::new(x_center + col_width * 0.5, y), label)
.color(if delta > 0 {
egui::Color32::from_rgb(50, 200, 100)
} else {
egui::Color32::from_rgb(255, 80, 80)
}),
);
}
}
} else {
// Draw GMP bars: single color going right from center
for (&bin_idx, bin) in &profile.gmp.bins {
let y = bin_idx as f64 * beta + beta * 0.5;
let w = bin.count as f64 * bar_scale;
let points = PlotPoints::new(vec![
[x_center - w * 0.5, y],
[x_center + w * 0.5, y],
]);
plot_ui.line(
Line::new(points)
.color(egui::Color32::from_rgb(100, 150, 255))
.width(2.5),
);
}
}
// Column separator
plot_ui.vline(
egui_plot::VLine::new(x_center - 0.5)
.color(egui::Color32::from_rgba_premultiplied(100, 100, 100, 40))
.width(0.5),
);
}
});
}