1use crate::aes::Aesthetic;
8use crate::coord::Coord;
9use crate::data::{DataFrame, Value};
10use crate::position::identity::PositionIdentity;
11use crate::position::Position;
12use crate::render::backend::{DrawBackend, LineStyle, Linetype, RectStyle};
13use crate::render::RenderError;
14use crate::scale::ScaleSet;
15use crate::stat::identity::StatIdentity;
16use crate::stat::Stat;
17use crate::theme::Theme;
18
19use super::{Geom, GeomParams};
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
23pub enum CandleStyle {
24 #[default]
26 Filled,
27 Hollow,
30}
31
32pub struct GeomCandlestick {
35 pub style: CandleStyle,
37 pub up: (u8, u8, u8),
39 pub down: (u8, u8, u8),
41 pub width: f64,
43 pub line_width: f64,
44 pub alpha: f64,
45}
46
47impl Default for GeomCandlestick {
48 fn default() -> Self {
49 GeomCandlestick {
50 style: CandleStyle::Filled,
51 up: (0x0c, 0xa6, 0x78),
52 down: (0xe0, 0x31, 0x31),
53 width: 0.6,
54 line_width: 1.0,
55 alpha: 1.0,
56 }
57 }
58}
59
60pub struct GeomOhlc {
63 pub open_tick: bool,
65 pub up: (u8, u8, u8),
66 pub down: (u8, u8, u8),
67 pub width: f64,
69 pub line_width: f64,
70 pub alpha: f64,
71}
72
73impl Default for GeomOhlc {
74 fn default() -> Self {
75 GeomOhlc {
76 open_tick: true,
77 up: (0x0c, 0xa6, 0x78),
78 down: (0xe0, 0x31, 0x31),
79 width: 0.6,
80 line_width: 1.4,
81 alpha: 1.0,
82 }
83 }
84}
85
86fn ohlc_required() -> Vec<Aesthetic> {
87 vec![
88 Aesthetic::X,
89 Aesthetic::Open,
90 Aesthetic::High,
91 Aesthetic::Low,
92 Aesthetic::Close,
93 ]
94}
95
96fn ohlc_setup(data: &mut DataFrame) {
98 for (src, dst) in [("low", "ymin"), ("high", "ymax")] {
99 if !data.has_column(dst) {
100 if let Some(c) = data.column(src) {
101 let c = c.to_vec();
102 data.add_column(dst.to_string(), c);
103 }
104 }
105 }
106}
107
108struct Period {
110 nx: f64,
111 open: f64,
112 high: f64,
113 low: f64,
114 close: f64,
115 up: bool,
116 up_from_prev: bool,
118}
119
120fn for_each_period(
123 data: &DataFrame,
124 scales: &ScaleSet,
125 width: f64,
126 backend: &mut dyn DrawBackend,
127 mut draw: impl FnMut(&Period, f64, &mut dyn DrawBackend) -> Result<(), RenderError>,
128) -> Result<(), RenderError> {
129 let col = |n: &str| {
130 data.column(n)
131 .ok_or_else(|| RenderError::MissingAesthetic(n.into()))
132 };
133 let (x_col, h_col, l_col, c_col) = (col("x")?, col("high")?, col("low")?, col("close")?);
134 let o_col = data.column("open").unwrap_or(c_col);
136 let x_scale = scales.get(&Aesthetic::X);
137 let y_scale = scales.get(&Aesthetic::Y);
138 let my = |v: f64| y_scale.map(|s| s.map(&Value::Float(v))).unwrap_or(0.0);
139 let half = if x_scale.map(|s| s.is_discrete()).unwrap_or(false) {
140 let n = x_scale.map(|s| s.breaks().len()).unwrap_or(1).max(1) as f64;
141 width / n / 2.0
142 } else {
143 super::continuous_bar_half_width(
144 x_col.iter().filter_map(|v| x_scale.map(|s| s.map(v))),
145 width,
146 0.01,
147 )
148 };
149 let x_mapper = super::support::XMapper::new(data, scales);
152 let nx_of = |i: usize| {
153 if x_scale.is_some() {
154 x_mapper.map(&x_col[i], i)
155 } else {
156 0.5
157 }
158 };
159 let mut order: Vec<usize> = (0..data.nrows()).collect();
160 order.sort_by(|&a, &b| nx_of(a).total_cmp(&nx_of(b)));
161 let mut prev_close: Option<f64> = None;
162 for i in order {
163 let (Some(o), Some(h), Some(l), Some(c)) = (
164 o_col[i].as_f64(),
165 h_col[i].as_f64(),
166 l_col[i].as_f64(),
167 c_col[i].as_f64(),
168 ) else {
169 continue;
170 };
171 let p = Period {
172 nx: nx_of(i),
173 open: my(o),
174 high: my(h),
175 low: my(l),
176 close: my(c),
177 up: c >= o,
178 up_from_prev: c >= prev_close.unwrap_or(o),
179 };
180 prev_close = Some(c);
181 let x = super::tip_value(&x_col[i]);
182 let f = |v: &Value| super::tip_value(v);
183 let tip = if data.has_column("open") {
184 format!(
185 "{x} — O {} H {} L {} C {}",
186 f(&o_col[i]),
187 f(&h_col[i]),
188 f(&l_col[i]),
189 f(&c_col[i])
190 )
191 } else {
192 format!(
193 "{x} — H {} L {} C {}",
194 f(&h_col[i]),
195 f(&l_col[i]),
196 f(&c_col[i])
197 )
198 };
199 let series =
200 super::series_key(data, i).or_else(|| Some(if p.up { "up" } else { "down" }.into()));
201 super::set_mark(
202 backend,
203 Some(tip),
204 Some(x),
205 series,
206 super::raw_value(&c_col[i]),
207 );
208 super::set_mark_formatted(
209 backend,
210 scales,
211 Some(&x_col[i]),
212 &Aesthetic::Y,
213 Some(&c_col[i]),
214 );
215 draw(&p, half / x_mapper.slot_n(i), backend)?;
216 }
217 super::clear_mark(backend);
218 Ok(())
219}
220
221impl Geom for GeomCandlestick {
222 fn draw(
223 &self,
224 data: &DataFrame,
225 coord: &dyn Coord,
226 scales: &ScaleSet,
227 _theme: &Theme,
228 backend: &mut dyn DrawBackend,
229 ) -> Result<(), RenderError> {
230 let area = backend.plot_area();
231 for_each_period(data, scales, self.width, backend, |p, half, b| {
232 let rising = match self.style {
233 CandleStyle::Filled => p.up,
234 CandleStyle::Hollow => p.up_from_prev,
235 };
236 let color = if rising { self.up } else { self.down };
237 let wick = [
238 coord.transform((p.nx, p.low), &area),
239 coord.transform((p.nx, p.high), &area),
240 ];
241 b.draw_line(
242 &wick,
243 &LineStyle {
244 color,
245 alpha: self.alpha,
246 width: self.line_width,
247 linetype: Linetype::Solid,
248 ..Default::default()
249 },
250 )?;
251 let (x0, y0) = coord.transform((p.nx - half, p.open.max(p.close)), &area);
252 let (x1, y1) = coord.transform((p.nx + half, p.open.min(p.close)), &area);
253 let (top, bottom) = (y0.min(y1), y0.max(y1).max(y0.min(y1) + 1.0));
255 let hollow = self.style == CandleStyle::Hollow && p.up;
257 b.draw_rect(
258 (x0.min(x1), top),
259 (x0.max(x1), bottom),
260 &RectStyle {
261 fill: if hollow {
262 Some((255, 255, 255))
263 } else {
264 Some(color)
265 },
266 stroke: Some(color),
267 stroke_width: if hollow {
268 self.line_width
269 } else {
270 self.line_width * 0.5
271 },
272 alpha: self.alpha,
273 clip: true,
274 corner_radius: 0.0,
275 },
276 )
277 })
278 }
279
280 fn required_aes(&self) -> Vec<Aesthetic> {
281 ohlc_required()
282 }
283 fn default_stat(&self) -> Box<dyn Stat> {
284 Box::new(StatIdentity)
285 }
286 fn default_position(&self) -> Box<dyn Position> {
287 Box::new(PositionIdentity)
288 }
289 fn default_params(&self) -> GeomParams {
290 GeomParams::default()
291 }
292 fn name(&self) -> &str {
293 "candlestick"
294 }
295 fn setup_data(&self, data: &mut DataFrame, _warnings: &mut Vec<String>) {
296 ohlc_setup(data);
297 }
298}
299
300impl GeomOhlc {
301 pub fn hlc() -> Self {
303 GeomOhlc {
304 open_tick: false,
305 ..Default::default()
306 }
307 }
308}
309
310impl Geom for GeomOhlc {
311 fn draw(
312 &self,
313 data: &DataFrame,
314 coord: &dyn Coord,
315 scales: &ScaleSet,
316 _theme: &Theme,
317 backend: &mut dyn DrawBackend,
318 ) -> Result<(), RenderError> {
319 let area = backend.plot_area();
320 for_each_period(data, scales, self.width, backend, |p, half, b| {
321 let style = LineStyle {
322 color: if p.up { self.up } else { self.down },
323 alpha: self.alpha,
324 width: self.line_width,
325 linetype: Linetype::Solid,
326 ..Default::default()
327 };
328 let t = |x: f64, y: f64| coord.transform((x, y), &area);
329 b.draw_line(&[t(p.nx, p.low), t(p.nx, p.high)], &style)?;
330 if self.open_tick {
331 b.draw_line(&[t(p.nx - half, p.open), t(p.nx, p.open)], &style)?;
332 }
333 b.draw_line(&[t(p.nx, p.close), t(p.nx + half, p.close)], &style)
334 })
335 }
336
337 fn required_aes(&self) -> Vec<Aesthetic> {
338 let mut req = ohlc_required();
339 if !self.open_tick {
340 req.retain(|a| *a != Aesthetic::Open);
341 }
342 req
343 }
344 fn default_stat(&self) -> Box<dyn Stat> {
345 Box::new(StatIdentity)
346 }
347 fn default_position(&self) -> Box<dyn Position> {
348 Box::new(PositionIdentity)
349 }
350 fn default_params(&self) -> GeomParams {
351 GeomParams::default()
352 }
353 fn name(&self) -> &str {
354 if self.open_tick {
355 "ohlc"
356 } else {
357 "hlc"
358 }
359 }
360 fn setup_data(&self, data: &mut DataFrame, _warnings: &mut Vec<String>) {
361 ohlc_setup(data);
362 }
363}