1use densa_core::{Color, Rect};
2use densa_draw::Canvas;
3
4use crate::series::{candles_within, points_within, with_neighbours};
5use crate::{Candles, Plot, Points};
6
7#[derive(Debug, Clone, Copy, PartialEq)]
9#[non_exhaustive]
10pub struct CandleStyle {
11 pub up: Color,
13 pub down: Color,
15 pub body: f32,
17}
18
19impl CandleStyle {
20 pub const fn new(up: Color, down: Color) -> Self {
22 Self {
23 up,
24 down,
25 body: DEFAULT_BODY,
26 }
27 }
28
29 #[must_use]
31 pub fn with_body(mut self, fraction: f32) -> Self {
32 self.body = usable_body(fraction);
33 self
34 }
35
36 fn color(&self, open: f64, close: f64) -> Color {
38 if close >= open {
39 self.up
40 } else {
41 self.down
42 }
43 }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq)]
49#[non_exhaustive]
50pub struct BarStyle {
51 pub body: f32,
53}
54
55impl BarStyle {
56 pub const fn new() -> Self {
58 Self { body: DEFAULT_BODY }
59 }
60
61 #[must_use]
63 pub fn with_body(mut self, fraction: f32) -> Self {
64 self.body = usable_body(fraction);
65 self
66 }
67}
68
69impl Default for BarStyle {
70 fn default() -> Self {
71 Self::new()
72 }
73}
74
75const DEFAULT_BODY: f32 = 0.7;
77
78fn usable_body(fraction: f32) -> f32 {
80 if fraction.is_finite() {
81 fraction.clamp(0.1, 1.0)
82 } else {
83 DEFAULT_BODY
84 }
85}
86
87const MIN_BODY_WITH_WICK: f64 = 3.0;
90
91#[derive(Debug, Clone, Copy)]
94struct Period {
95 left: f64,
96 top: f64,
97 bottom: f64,
98 open: f64,
99 close: f64,
100}
101
102pub fn paint_candles<C: Candles + ?Sized>(
113 canvas: &mut Canvas<'_>,
114 plot: &Plot,
115 candles: &C,
116 interval: f64,
117 style: CandleStyle,
118) {
119 let area = plot.area();
120 if area.is_empty() {
121 return;
122 }
123 let state = canvas.state();
124 canvas.clip(area);
125 let width = bar_width(plot, interval, style.body);
126 let mut merged: Option<Period> = None;
127 for index in with_neighbours(candles_within(candles, &plot.x()), candles.len()) {
128 let candle = candles.candle(index);
129 if !candle.is_finite() {
130 continue;
131 }
132 let left = (plot.x_at(candles.x(index)) - width / 2.0).round();
133 let high = plot.clamp_y(plot.y_at(candle.high));
134 let low = plot.clamp_y(plot.y_at(candle.low));
135 if width < MIN_BODY_WITH_WICK {
136 match merged.as_mut() {
137 Some(period) if period.left == left => {
138 period.top = period.top.min(high);
139 period.bottom = period.bottom.max(low);
140 period.close = candle.close;
141 }
142 _ => {
143 if let Some(period) = merged.take() {
144 fill_period(canvas, period, width, style);
145 }
146 merged = Some(Period {
147 left,
148 top: high,
149 bottom: low,
150 open: candle.open,
151 close: candle.close,
152 });
153 }
154 }
155 continue;
156 }
157 let color = style.color(candle.open, candle.close);
158 let wick = left + (width / 2.0).floor();
159 fill_bar(canvas, wick, 1.0, high, low, color);
160 let open = plot.clamp_y(plot.y_at(candle.open));
161 let close = plot.clamp_y(plot.y_at(candle.close));
162 fill_bar(canvas, left, width, open.min(close), open.max(close), color);
163 }
164 if let Some(period) = merged {
165 fill_period(canvas, period, width, style);
166 }
167 canvas.restore(state);
168}
169
170fn fill_period(canvas: &mut Canvas<'_>, period: Period, width: f64, style: CandleStyle) {
172 let color = style.color(period.open, period.close);
173 fill_bar(canvas, period.left, width, period.top, period.bottom, color);
174}
175
176#[derive(Debug, Clone, Copy)]
179struct Stack {
180 left: f64,
181 top: f64,
182 bottom: f64,
183 color: Color,
184 height: f64,
185}
186
187pub fn paint_bars<P: Points + ?Sized>(
195 canvas: &mut Canvas<'_>,
196 plot: &Plot,
197 bars: &P,
198 interval: f64,
199 style: BarStyle,
200 color: impl Fn(usize) -> Color,
201) {
202 let area = plot.area();
203 if area.is_empty() {
204 return;
205 }
206 let state = canvas.state();
207 canvas.clip(area);
208 let width = bar_width(plot, interval, style.body);
209 let base = plot.clamp_y(plot.y_at(0.0));
210 let mut merged: Option<Stack> = None;
211 for index in with_neighbours(points_within(bars, &plot.x()), bars.len()) {
212 let y = bars.y(index);
213 if !y.is_finite() {
214 continue;
215 }
216 let left = (plot.x_at(bars.x(index)) - width / 2.0).round();
217 let end = plot.clamp_y(plot.y_at(y));
218 let (top, bottom) = (end.min(base), end.max(base));
219 match merged.as_mut() {
220 Some(stack) if stack.left == left => {
221 stack.top = stack.top.min(top);
222 stack.bottom = stack.bottom.max(bottom);
223 if bottom - top > stack.height {
224 stack.height = bottom - top;
225 stack.color = color(index);
226 }
227 }
228 _ => {
229 if let Some(stack) = merged.take() {
230 fill_stack(canvas, stack, width);
231 }
232 merged = Some(Stack {
233 left,
234 top,
235 bottom,
236 color: color(index),
237 height: bottom - top,
238 });
239 }
240 }
241 }
242 if let Some(stack) = merged {
243 fill_stack(canvas, stack, width);
244 }
245 canvas.restore(state);
246}
247
248fn fill_stack(canvas: &mut Canvas<'_>, stack: Stack, width: f64) {
250 fill_bar(
251 canvas,
252 stack.left,
253 width,
254 stack.top,
255 stack.bottom,
256 stack.color,
257 );
258}
259
260const WIDTH_EPSILON: f64 = 1e-4;
263
264fn bar_width(plot: &Plot, interval: f64, fraction: f32) -> f64 {
266 let spacing = plot.x_length(interval).abs();
267 let width = (spacing * f64::from(usable_body(fraction)) + WIDTH_EPSILON).floor();
268 if width.is_finite() {
269 width.clamp(1.0, f64::from(plot.area().width()).max(1.0))
271 } else {
272 1.0
273 }
274}
275
276fn fill_bar(canvas: &mut Canvas<'_>, left: f64, width: f64, top: f64, bottom: f64, color: Color) {
279 let top = top.round();
280 let bottom = bottom.round().max(top + 1.0);
281 let rect = Rect::from_edges(
283 left as f32,
284 top as f32,
285 (left + width) as f32,
286 bottom as f32,
287 );
288 canvas.fill_rect(rect, color);
289}