Skip to main content

egui_charts/drawings/domain/
gann.rs

1//! Saved levels, fan lines, arcs and background of the Gann tools.
2
3use super::DrawingToolType;
4use serde::{Deserialize, Serialize};
5use std::cell::Cell;
6use std::f32::consts::SQRT_2;
7
8/// Units per side of a Gann square and of a fixed Gann square.
9pub(crate) const GANN_SQUARE_UNITS: f32 = 5.0;
10/// An arc of this radius in units, or more, lies wholly outside a Gann square.
11pub(crate) const GANN_MAX_ARC: f32 = GANN_SQUARE_UNITS * SQRT_2;
12
13const GREY: [u8; 4] = [128, 128, 128, 255];
14const ORANGE: [u8; 4] = [255, 152, 0, 255];
15const CYAN: [u8; 4] = [0, 188, 212, 255];
16const GREEN: [u8; 4] = [76, 175, 80, 255];
17const TEAL: [u8; 4] = [8, 153, 129, 255];
18const BLUE: [u8; 4] = [41, 98, 255, 255];
19const RED: [u8; 4] = [242, 54, 69, 255];
20const PINK: [u8; 4] = [233, 30, 99, 255];
21const PURPLE: [u8; 4] = [156, 39, 176, 255];
22
23/// The corners the fan lines of a Gann square start from.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
25pub enum GannFanDirection {
26    /// From the first anchor towards the opposite corner.
27    #[default]
28    Forward,
29    /// From the opposite corner back towards the first anchor.
30    Reverse,
31    /// From both corners.
32    Both,
33}
34
35/// One line of a Gann tool: a price or time level, a fan line or an arc. The [`GannConfig`]
36/// field that holds it says how its value is read.
37#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
38#[serde(from = "SavedGannLevel")]
39pub struct GannLevel {
40    /// Where the line lies, in the unit of the field that holds it.
41    pub value: f32,
42    /// Whether the line, its label and the band it closes are drawn.
43    pub visible: bool,
44    /// Line, label and band colour as RGBA.
45    pub color: [u8; 4],
46}
47
48impl GannLevel {
49    /// A visible level.
50    pub fn new(value: f32, color: [u8; 4]) -> Self {
51        Self {
52            value,
53            visible: true,
54            color,
55        }
56    }
57}
58
59/// A saved level: a full entry, or a bare value as drawings saved before levels had a
60/// visibility and colour hold it.
61#[derive(Deserialize)]
62#[serde(untagged)]
63enum SavedGannLevel {
64    Level {
65        value: f32,
66        visible: bool,
67        color: [u8; 4],
68    },
69    Value(f32),
70}
71
72impl From<SavedGannLevel> for GannLevel {
73    fn from(saved: SavedGannLevel) -> Self {
74        match saved {
75            SavedGannLevel::Level {
76                value,
77                visible,
78                color,
79            } => Self {
80                value,
81                visible,
82                color,
83            },
84            // The drawing colour is not known here, so older values take the neutral level colour.
85            SavedGannLevel::Value(value) => Self::new(value, GREY),
86        }
87    }
88}
89
90/// Levels, fan lines and arcs of the Gann tools, and the background between them.
91///
92/// A field missing from a saved object takes the value of [`GannConfig::for_tool`] for the tool
93/// of the [`Drawing`](crate::drawings::Drawing) that holds it, or of [`GannConfig::default`]
94/// when the object is read on its own.
95#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
96#[serde(default)]
97pub struct GannConfig {
98    /// Price levels counted from the first anchor: for a Gann box a fraction of its height from
99    /// 0 to 1, for a Gann square or fixed square units from 0 to 5. Others are not drawn.
100    #[serde(default = "left_out_price_levels")]
101    pub price_levels: Vec<GannLevel>,
102    /// Time levels counted from the first anchor, read as `price_levels` are.
103    #[serde(default = "left_out_time_levels")]
104    pub time_levels: Vec<GannLevel>,
105    /// Whether a Gann square draws its price and time levels.
106    pub show_grid: bool,
107    /// Whether a Gann square draws its fan lines.
108    pub show_fan: bool,
109    /// Fan lines of a Gann square, and the lines of a Gann fan: price units per time unit, where
110    /// 1 is the 1x1 line. Values must be positive and finite.
111    #[serde(default = "left_out_fan_ratios")]
112    pub fan_ratios: Vec<GannLevel>,
113    /// The corners the fan lines of a Gann square start from.
114    pub fan_direction: GannFanDirection,
115    /// Whether a Gann square draws its arcs.
116    pub show_arcs: bool,
117    /// Arcs of a Gann square around its first anchor, as radii in units. Values must be positive
118    /// and finite; the part outside the square is not drawn.
119    pub arc_ratios: Vec<GannLevel>,
120    /// Whether bands are tinted: between the levels of a box, the arcs of a square and the
121    /// lines of a fan, each in the colour of the band's outer line.
122    pub show_background: bool,
123    /// Band opacity, from 0 to 255.
124    pub background_opacity: u8,
125    /// Whether a Gann box draws rays from its corners to the level points on its far edges.
126    pub show_angles: bool,
127}
128
129thread_local! {
130    /// The fields whose default differs between the Gann tools that the last [`GannConfig`] read
131    /// on this thread left out: its price levels, time levels and fan lines.
132    static LEFT_OUT: Cell<[bool; 3]> = const { Cell::new([false; 3]) };
133}
134
135/// Notes that the configuration being read left out `field`, and gives its [`Default`] value.
136fn left_out<T>(field: usize, value: fn(GannConfig) -> T) -> T {
137    LEFT_OUT.with(|left_out| {
138        let mut fields = left_out.get();
139        fields[field] = true;
140        left_out.set(fields);
141    });
142    value(GannConfig::default())
143}
144
145fn left_out_price_levels() -> Vec<GannLevel> {
146    left_out(0, |config| config.price_levels)
147}
148
149fn left_out_time_levels() -> Vec<GannLevel> {
150    left_out(1, |config| config.time_levels)
151}
152
153fn left_out_fan_ratios() -> Vec<GannLevel> {
154    left_out(2, |config| config.fan_ratios)
155}
156
157fn levels(entries: &[(f32, [u8; 4])]) -> Vec<GannLevel> {
158    entries
159        .iter()
160        .map(|&(value, color)| GannLevel::new(value, color))
161        .collect()
162}
163
164impl Default for GannConfig {
165    /// The Gann box's configuration, as [`GannConfig::for_tool`] gives it. A Gann square, fixed
166    /// square or fan reads its levels or fan lines differently and starts from `for_tool` of its
167    /// own tool.
168    fn default() -> Self {
169        let box_levels = levels(&[
170            (0.0, GREY),
171            (0.25, ORANGE),
172            (0.382, CYAN),
173            (0.5, GREEN),
174            (0.618, TEAL),
175            (0.75, BLUE),
176            (1.0, GREY),
177        ]);
178        let mut fan_ratios = levels(&[
179            (8.0, GREY),
180            (5.0, GREY),
181            (4.0, GREY),
182            (3.0, GREY),
183            (2.0, CYAN),
184            (1.0, GREEN),
185            (0.5, TEAL),
186            (1.0 / 3.0, GREY),
187            (0.25, GREY),
188            (0.2, GREY),
189            (0.125, GREY),
190        ]);
191        for line in &mut fan_ratios {
192            line.visible = matches!(line.value, 2.0 | 1.0 | 0.5);
193        }
194        Self {
195            price_levels: box_levels.clone(),
196            time_levels: box_levels,
197            show_grid: true,
198            show_fan: true,
199            fan_ratios,
200            fan_direction: GannFanDirection::default(),
201            show_arcs: true,
202            arc_ratios: [ORANGE, CYAN, GREEN, TEAL, BLUE]
203                .into_iter()
204                .zip(1..=5)
205                .flat_map(|(color, n)| {
206                    let n = n as f32;
207                    [
208                        GannLevel::new(n, color),
209                        GannLevel::new((n * n + 1.0).sqrt(), color),
210                    ]
211                })
212                .collect(),
213            show_background: true,
214            background_opacity: 51,
215            show_angles: false,
216        }
217    }
218}
219
220impl GannConfig {
221    /// The levels, fan lines and arcs a new Gann box, square, fixed square or fan starts from,
222    /// which a drawing without [`Drawing::gann`](crate::drawings::Drawing::gann) draws. Start a
223    /// configuration from the drawing's own tool here rather than from
224    /// [`GannConfig::default`], which is the Gann box's.
225    pub fn for_tool(tool: DrawingToolType) -> Self {
226        match tool {
227            DrawingToolType::GannSquare | DrawingToolType::GannFixed => {
228                let units = levels(&[
229                    (0.0, GREY),
230                    (1.0, ORANGE),
231                    (2.0, CYAN),
232                    (3.0, GREEN),
233                    (4.0, TEAL),
234                    (5.0, GREY),
235                ]);
236                Self {
237                    price_levels: units.clone(),
238                    time_levels: units,
239                    ..Self::default()
240                }
241            }
242            DrawingToolType::GannFan => Self {
243                fan_ratios: levels(&[
244                    (8.0, RED),
245                    (4.0, PINK),
246                    (3.0, PURPLE),
247                    (2.0, BLUE),
248                    (1.0, CYAN),
249                    (0.5, TEAL),
250                    (1.0 / 3.0, GREEN),
251                    (0.25, TEAL),
252                    (0.125, ORANGE),
253                ]),
254                ..Self::default()
255            },
256            _ => Self::default(),
257        }
258    }
259
260    /// Forgets the fields an earlier configuration read on this thread left out, before a
261    /// drawing that may hold one is read.
262    pub(crate) fn forget_left_out() {
263        LEFT_OUT.with(|left_out| left_out.set([false; 3]));
264    }
265
266    /// Takes the fields that this configuration, the last one read on this thread, left out from
267    /// [`GannConfig::for_tool`] of `tool` instead of [`GannConfig::default`].
268    pub(crate) fn fill_left_out(&mut self, tool: DrawingToolType) {
269        let [price_levels, time_levels, fan_ratios] = LEFT_OUT.with(|l| l.replace([false; 3]));
270        if !(price_levels || time_levels || fan_ratios) {
271            return;
272        }
273        let defaults = Self::for_tool(tool);
274        if price_levels {
275            self.price_levels = defaults.price_levels;
276        }
277        if time_levels {
278            self.time_levels = defaults.time_levels;
279        }
280        if fan_ratios {
281            self.fan_ratios = defaults.fan_ratios;
282        }
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289
290    /// The defaults a new tool draws: the square's three fan lines on, its arcs at n and the
291    /// square root of n² + 1 units, and the fan's nine lines. Radii by hand: √2, √5, √10, √17, √26.
292    #[test]
293    fn new_tools_start_from_their_defaults() {
294        let square = GannConfig::for_tool(DrawingToolType::GannSquare);
295        let visible: Vec<f32> = square
296            .fan_ratios
297            .iter()
298            .filter(|l| l.visible)
299            .map(|l| l.value)
300            .collect();
301        assert_eq!(visible, [2.0, 1.0, 0.5]);
302        assert_eq!(square.fan_ratios.len(), 11);
303        let radii = [
304            1.0, SQRT_2, 2.0, 2.236068, 3.0, 3.1622777, 4.0, 4.1231055, 5.0, 5.0990195,
305        ];
306        assert_eq!(square.arc_ratios.len(), radii.len());
307        for (arc, radius) in square.arc_ratios.iter().zip(radii) {
308            assert!((arc.value - radius).abs() < 1e-5, "{arc:?} != {radius}");
309        }
310        let fan = GannConfig::for_tool(DrawingToolType::GannFan);
311        assert_eq!(fan.fan_ratios.iter().filter(|l| l.visible).count(), 9);
312    }
313}