Skip to main content

ggplot_rs/geom/
renko.rs

1//! Renko and point-and-figure charts: `geom_renko` (bricks) and
2//! `geom_point_figure` (columns of X and O). Map `y` to the price in time
3//! order; the stats ([`StatRenko`], [`StatPointFigure`]) replace time with
4//! brick / column numbers.
5
6use crate::aes::Aesthetic;
7use crate::coord::Coord;
8use crate::data::{DataFrame, Value};
9use crate::position::identity::PositionIdentity;
10use crate::position::Position;
11use crate::render::backend::{DrawBackend, LineStyle, Linetype, PointStyle, RectStyle};
12use crate::render::RenderError;
13use crate::scale::ScaleSet;
14use crate::stat::renko::{StatPointFigure, StatRenko};
15use crate::stat::Stat;
16use crate::theme::Theme;
17
18use super::{Geom, GeomParams};
19
20/// Renko (`point_figure: false`) or point-and-figure chart.
21pub struct GeomRenko {
22    pub box_size: f64,
23    /// Point-and-figure boxes needed to reverse a column.
24    pub reversal: usize,
25    pub point_figure: bool,
26    pub up: (u8, u8, u8),
27    pub down: (u8, u8, u8),
28}
29
30impl GeomRenko {
31    pub fn renko(box_size: f64) -> Self {
32        GeomRenko {
33            box_size,
34            reversal: 3,
35            point_figure: false,
36            up: (0x0c, 0xa6, 0x78),
37            down: (0xe0, 0x31, 0x31),
38        }
39    }
40
41    pub fn point_figure(box_size: f64, reversal: usize) -> Self {
42        GeomRenko {
43            reversal,
44            point_figure: true,
45            ..Self::renko(box_size)
46        }
47    }
48}
49
50impl Geom for GeomRenko {
51    fn draw(
52        &self,
53        data: &DataFrame,
54        coord: &dyn Coord,
55        scales: &ScaleSet,
56        _theme: &Theme,
57        backend: &mut dyn DrawBackend,
58    ) -> Result<(), RenderError> {
59        let col = |n: &str| {
60            data.column(n)
61                .ok_or_else(|| RenderError::MissingAesthetic(n.into()))
62        };
63        let (xc, y0c, y1c) = (col("x")?, col("ymin")?, col("ymax")?);
64        let kind = if self.point_figure {
65            col("mark")?
66        } else {
67            col("direction")?
68        };
69        let area = backend.plot_area();
70        let m = |aes: Aesthetic, v: &Value| scales.get(&aes).map(|s| s.map(v)).unwrap_or(0.0);
71        let half = scales
72            .get(&Aesthetic::X)
73            .map(|s| {
74                let d = s.map(&Value::Float(2.0)) - s.map(&Value::Float(1.0));
75                0.42 * d.abs()
76            })
77            .unwrap_or(0.01);
78        for i in 0..data.nrows() {
79            let nx = m(Aesthetic::X, &xc[i]);
80            let up = matches!(kind[i].key_str().as_ref(), "up" | "X");
81            let color = if up { self.up } else { self.down };
82            let a = coord.transform((nx - half, m(Aesthetic::Y, &y1c[i])), &area);
83            let b = coord.transform((nx + half, m(Aesthetic::Y, &y0c[i])), &area);
84            let (l, t, r, btm) = (a.0.min(b.0), a.1.min(b.1), a.0.max(b.0), a.1.max(b.1));
85            super::set_mark(
86                backend,
87                Some(format!(
88                    "{} {}–{}",
89                    kind[i].key_str(),
90                    super::tip_value(&y0c[i]),
91                    super::tip_value(&y1c[i])
92                )),
93                Some(super::tip_value(&xc[i])),
94                Some(kind[i].key_str().into_owned()),
95                super::raw_value(&y1c[i]),
96            );
97            if !self.point_figure {
98                backend.draw_rect(
99                    (l, t),
100                    (r, btm),
101                    &RectStyle {
102                        fill: Some(color),
103                        stroke: Some((255, 255, 255)),
104                        stroke_width: 0.5,
105                        alpha: 1.0,
106                        clip: true,
107                        corner_radius: 0.0,
108                    },
109                )?;
110            } else if up {
111                let s = LineStyle {
112                    color,
113                    alpha: 1.0,
114                    width: 1.5,
115                    linetype: Linetype::Solid,
116                    ..Default::default()
117                };
118                let inset = 0.15 * (r - l).min(btm - t);
119                backend.draw_line(&[(l + inset, t + inset), (r - inset, btm - inset)], &s)?;
120                backend.draw_line(&[(l + inset, btm - inset), (r - inset, t + inset)], &s)?;
121            } else {
122                backend.draw_circle(
123                    ((l + r) / 2.0, (t + btm) / 2.0),
124                    0.38 * (r - l).min(btm - t),
125                    &PointStyle {
126                        color,
127                        alpha: 1.0,
128                        filled: false,
129                        ..Default::default()
130                    },
131                )?;
132            }
133        }
134        super::clear_mark(backend);
135        Ok(())
136    }
137
138    fn required_aes(&self) -> Vec<Aesthetic> {
139        vec![Aesthetic::Y]
140    }
141    fn default_stat(&self) -> Box<dyn Stat> {
142        if self.point_figure {
143            Box::new(StatPointFigure {
144                box_size: self.box_size,
145                reversal: self.reversal,
146            })
147        } else {
148            Box::new(StatRenko {
149                box_size: self.box_size,
150            })
151        }
152    }
153    fn default_position(&self) -> Box<dyn Position> {
154        Box::new(PositionIdentity)
155    }
156    fn default_params(&self) -> GeomParams {
157        GeomParams::default()
158    }
159    fn name(&self) -> &str {
160        if self.point_figure {
161            "point_figure"
162        } else {
163            "renko"
164        }
165    }
166}