use densa_core::{Color, Rect};
use densa_draw::Canvas;
use crate::series::{candles_within, points_within, with_neighbours};
use crate::{Candles, Plot, Points};
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct CandleStyle {
pub up: Color,
pub down: Color,
pub body: f32,
}
impl CandleStyle {
pub const fn new(up: Color, down: Color) -> Self {
Self {
up,
down,
body: DEFAULT_BODY,
}
}
#[must_use]
pub fn with_body(mut self, fraction: f32) -> Self {
self.body = usable_body(fraction);
self
}
fn color(&self, open: f64, close: f64) -> Color {
if close >= open {
self.up
} else {
self.down
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct BarStyle {
pub body: f32,
}
impl BarStyle {
pub const fn new() -> Self {
Self { body: DEFAULT_BODY }
}
#[must_use]
pub fn with_body(mut self, fraction: f32) -> Self {
self.body = usable_body(fraction);
self
}
}
impl Default for BarStyle {
fn default() -> Self {
Self::new()
}
}
const DEFAULT_BODY: f32 = 0.7;
fn usable_body(fraction: f32) -> f32 {
if fraction.is_finite() {
fraction.clamp(0.1, 1.0)
} else {
DEFAULT_BODY
}
}
const MIN_BODY_WITH_WICK: f64 = 3.0;
#[derive(Debug, Clone, Copy)]
struct Period {
left: f64,
top: f64,
bottom: f64,
open: f64,
close: f64,
}
pub fn paint_candles<C: Candles + ?Sized>(
canvas: &mut Canvas<'_>,
plot: &Plot,
candles: &C,
interval: f64,
style: CandleStyle,
) {
let area = plot.area();
if area.is_empty() {
return;
}
let state = canvas.state();
canvas.clip(area);
let width = bar_width(plot, interval, style.body);
let mut merged: Option<Period> = None;
for index in with_neighbours(candles_within(candles, &plot.x()), candles.len()) {
let candle = candles.candle(index);
if !candle.is_finite() {
continue;
}
let left = (plot.x_at(candles.x(index)) - width / 2.0).round();
let high = plot.clamp_y(plot.y_at(candle.high));
let low = plot.clamp_y(plot.y_at(candle.low));
if width < MIN_BODY_WITH_WICK {
match merged.as_mut() {
Some(period) if period.left == left => {
period.top = period.top.min(high);
period.bottom = period.bottom.max(low);
period.close = candle.close;
}
_ => {
if let Some(period) = merged.take() {
fill_period(canvas, period, width, style);
}
merged = Some(Period {
left,
top: high,
bottom: low,
open: candle.open,
close: candle.close,
});
}
}
continue;
}
let color = style.color(candle.open, candle.close);
let wick = left + (width / 2.0).floor();
fill_bar(canvas, wick, 1.0, high, low, color);
let open = plot.clamp_y(plot.y_at(candle.open));
let close = plot.clamp_y(plot.y_at(candle.close));
fill_bar(canvas, left, width, open.min(close), open.max(close), color);
}
if let Some(period) = merged {
fill_period(canvas, period, width, style);
}
canvas.restore(state);
}
fn fill_period(canvas: &mut Canvas<'_>, period: Period, width: f64, style: CandleStyle) {
let color = style.color(period.open, period.close);
fill_bar(canvas, period.left, width, period.top, period.bottom, color);
}
#[derive(Debug, Clone, Copy)]
struct Stack {
left: f64,
top: f64,
bottom: f64,
color: Color,
height: f64,
}
pub fn paint_bars<P: Points + ?Sized>(
canvas: &mut Canvas<'_>,
plot: &Plot,
bars: &P,
interval: f64,
style: BarStyle,
color: impl Fn(usize) -> Color,
) {
let area = plot.area();
if area.is_empty() {
return;
}
let state = canvas.state();
canvas.clip(area);
let width = bar_width(plot, interval, style.body);
let base = plot.clamp_y(plot.y_at(0.0));
let mut merged: Option<Stack> = None;
for index in with_neighbours(points_within(bars, &plot.x()), bars.len()) {
let y = bars.y(index);
if !y.is_finite() {
continue;
}
let left = (plot.x_at(bars.x(index)) - width / 2.0).round();
let end = plot.clamp_y(plot.y_at(y));
let (top, bottom) = (end.min(base), end.max(base));
match merged.as_mut() {
Some(stack) if stack.left == left => {
stack.top = stack.top.min(top);
stack.bottom = stack.bottom.max(bottom);
if bottom - top > stack.height {
stack.height = bottom - top;
stack.color = color(index);
}
}
_ => {
if let Some(stack) = merged.take() {
fill_stack(canvas, stack, width);
}
merged = Some(Stack {
left,
top,
bottom,
color: color(index),
height: bottom - top,
});
}
}
}
if let Some(stack) = merged {
fill_stack(canvas, stack, width);
}
canvas.restore(state);
}
fn fill_stack(canvas: &mut Canvas<'_>, stack: Stack, width: f64) {
fill_bar(
canvas,
stack.left,
width,
stack.top,
stack.bottom,
stack.color,
);
}
const WIDTH_EPSILON: f64 = 1e-4;
fn bar_width(plot: &Plot, interval: f64, fraction: f32) -> f64 {
let spacing = plot.x_length(interval).abs();
let width = (spacing * f64::from(usable_body(fraction)) + WIDTH_EPSILON).floor();
if width.is_finite() {
width.clamp(1.0, f64::from(plot.area().width()).max(1.0))
} else {
1.0
}
}
fn fill_bar(canvas: &mut Canvas<'_>, left: f64, width: f64, top: f64, bottom: f64, color: Color) {
let top = top.round();
let bottom = bottom.round().max(top + 1.0);
let rect = Rect::from_edges(
left as f32,
top as f32,
(left + width) as f32,
bottom as f32,
);
canvas.fill_rect(rect, color);
}