use crate::data::{ChartData, RangeSeries};
use crate::error::Result;
use crate::layers::{Layer, LayerStage};
use crate::renderer::RenderContext;
use crate::style::ChartStyle;
use crate::theme::{ChartTheme, Color};
use crate::viewport::{Rect, Viewport};
#[derive(Debug, Clone)]
pub struct AuxiliaryBarConfig {
pub height_factor: f32,
pub bar_width_factor: f32,
pub min_width: f32,
pub max_width: f32,
}
impl Default for AuxiliaryBarConfig {
fn default() -> Self {
Self {
height_factor: 0.2, bar_width_factor: 0.8, min_width: 1.0,
max_width: 20.0,
}
}
}
#[derive(Debug, Clone)]
struct BarGeometry {
rect: Rect,
color: Color,
}
#[derive(Debug)]
pub struct AuxiliaryBarLayer {
enabled: bool,
needs_render: bool,
config: AuxiliaryBarConfig,
bars: Vec<BarGeometry>,
auxiliary_data: Option<AuxiliaryData>,
}
#[derive(Debug, Clone)]
struct AuxiliaryData {
timestamps: Vec<f64>,
values: Vec<f64>,
is_positive: Option<Vec<bool>>,
}
impl AuxiliaryBarLayer {
pub fn new() -> Self {
Self {
enabled: true,
needs_render: true,
config: AuxiliaryBarConfig::default(),
bars: Vec::new(),
auxiliary_data: None,
}
}
pub fn with_config(config: AuxiliaryBarConfig) -> Self {
let mut layer = Self::new();
layer.config = config;
layer
}
pub fn set_auxiliary_data(
&mut self,
timestamps: Vec<f64>,
values: Vec<f64>,
is_positive: Option<Vec<bool>>,
) {
self.auxiliary_data = Some(AuxiliaryData {
timestamps,
values,
is_positive,
});
self.needs_render = true;
}
fn calculate_bars_from_data(&mut self, viewport: &Viewport, theme: &ChartTheme) {
self.bars.clear();
let data = match &self.auxiliary_data {
Some(d) => d,
None => return,
};
if data.timestamps.is_empty() || data.values.is_empty() {
return;
}
let count = data.timestamps.len().min(data.values.len());
let max_value = data
.values
.iter()
.take(count)
.fold(0.0f64, |a, &b| a.max(b));
if max_value <= 0.0 {
return;
}
let chart_rect = viewport.chart_content_rect();
let volume_rect = viewport.volume_rect();
let time_duration = viewport.chart_bounds.time_duration().num_seconds() as f32;
if time_duration <= 0.0 {
return;
}
let time_scale = chart_rect.width / time_duration;
let time_spacing = if count > 1 {
let first = data.timestamps[0] as f32;
let last = data.timestamps[count - 1] as f32;
(last - first) / (count - 1) as f32
} else {
60.0
};
let screen_time_width = time_spacing * time_scale;
let bar_width = (screen_time_width * self.config.bar_width_factor)
.max(self.config.min_width)
.min(self.config.max_width);
let half_width = bar_width * 0.5;
let time_start = viewport.chart_bounds.time_start.timestamp() as f32;
for i in 0..count {
let timestamp = data.timestamps[i];
let value = data.values[i];
let delta = timestamp as f32 - time_start;
let x_center = chart_rect.x + delta * time_scale;
let value_ratio = (value / max_value).clamp(0.0, 1.0) as f32;
let desired_height = (volume_rect.height * 0.95) * value_ratio;
let final_height = desired_height.max(2.0);
let rect = Rect::new(
x_center - half_width,
volume_rect.y + volume_rect.height - final_height,
bar_width,
final_height,
);
let is_positive = data
.is_positive
.as_ref()
.map(|v| v.get(i).copied().unwrap_or(true))
.unwrap_or(true);
let color = if is_positive {
theme.colors.volume_bullish
} else {
theme.colors.volume_bearish
};
self.bars.push(BarGeometry { rect, color });
}
self.needs_render = !self.bars.is_empty();
}
fn _calculate_bars_from_series<S: RangeSeries + ?Sized>(
&mut self,
series: &S,
data: &ChartData,
viewport: &Viewport,
theme: &ChartTheme,
) {
self.bars.clear();
let (start_idx, end_idx) = match data.visible_indices() {
Some((start, end)) => (start, end),
None => return,
};
if start_idx >= end_idx {
return;
}
let mut max_value = 0.0f64;
for i in start_idx..end_idx {
if let Some(aux) = series.get_auxiliary(i) {
if aux > max_value {
max_value = aux;
}
}
}
if max_value <= 0.0 {
return;
}
let chart_rect = viewport.chart_content_rect();
let volume_rect = viewport.volume_rect();
let time_duration = viewport.chart_bounds.time_duration().num_seconds() as f32;
if time_duration <= 0.0 {
return;
}
let time_scale = chart_rect.width / time_duration;
let count = end_idx - start_idx;
let time_spacing = if count > 1 {
let first = series.get_x(start_idx) as f32;
let last = series.get_x(end_idx - 1) as f32;
(last - first) / (count - 1) as f32
} else {
60.0
};
let screen_time_width = time_spacing * time_scale;
let bar_width = (screen_time_width * self.config.bar_width_factor)
.max(self.config.min_width)
.min(self.config.max_width);
let half_width = bar_width * 0.5;
let time_start = viewport.chart_bounds.time_start.timestamp() as f32;
for i in start_idx..end_idx {
let aux = match series.get_auxiliary(i) {
Some(v) => v,
None => continue,
};
let (start_val, _, _, end_val) = series.get_range(i);
let timestamp_f64 = series.get_x(i);
let delta = timestamp_f64 as f32 - time_start;
let x_center = chart_rect.x + delta * time_scale;
let value_ratio = (aux / max_value).clamp(0.0, 1.0) as f32;
let desired_height = (volume_rect.height * 0.95) * value_ratio;
let final_height = desired_height.max(2.0);
let rect = Rect::new(
x_center - half_width,
volume_rect.y + volume_rect.height - final_height,
bar_width,
final_height,
);
let is_positive = end_val >= start_val;
let color = if is_positive {
theme.colors.volume_bullish
} else {
theme.colors.volume_bearish
};
self.bars.push(BarGeometry { rect, color });
}
self.needs_render = !self.bars.is_empty();
}
}
impl Default for AuxiliaryBarLayer {
fn default() -> Self {
Self::new()
}
}
impl Layer for AuxiliaryBarLayer {
fn name(&self) -> &str {
"AuxiliaryBar"
}
fn stage(&self) -> LayerStage {
LayerStage::VolumePane
}
fn update(
&mut self,
_data: &ChartData,
viewport: &Viewport,
theme: &ChartTheme,
_style: &ChartStyle,
) {
if self.auxiliary_data.is_some() {
self.calculate_bars_from_data(viewport, theme);
return;
}
self.bars.clear();
self.needs_render = false;
}
fn render(
&self,
context: &mut RenderContext,
_render_pass: &mut wgpu::RenderPass,
) -> Result<()> {
if !self.enabled || self.bars.is_empty() {
return Ok(());
}
let volume_rect = context.viewport().volume_rect();
let (background_darken, baseline_alpha) = {
let s = &context.style().volume;
(s.background_darken, s.baseline_alpha)
};
let background = context
.theme()
.colors
.chart_background
.darken(background_darken);
context.draw_rect(volume_rect, background);
context.draw_line(
[volume_rect.x, volume_rect.y],
[volume_rect.x + volume_rect.width, volume_rect.y],
context.theme().colors.grid_major.with_alpha(baseline_alpha),
1.0,
);
for bar in &self.bars {
context.draw_rect(bar.rect, bar.color);
}
Ok(())
}
fn needs_render(&self) -> bool {
self.needs_render
}
fn z_order(&self) -> i32 {
-50 }
fn is_enabled(&self) -> bool {
self.enabled
}
fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
self.needs_render = true;
}
}
pub type VolumeLayer = AuxiliaryBarLayer;
pub type VolumeConfig = AuxiliaryBarConfig;