Skip to main content

egui_charts/widget/
readout.rs

1//! Study readouts for host data windows and chart tooltips.
2
3use super::{Chart, SeriesId};
4use crate::chart::rendering::StudyValue;
5
6impl Chart {
7    /// Takes the source preselection requested by a study menu's Add indicator action.
8    /// Open the host indicator chooser with this source, then call `set_indicator_source`
9    /// on the added study. The chart owns no chooser or account service.
10    pub fn take_indicator_source_request(&mut self) -> Option<crate::studies::StudySource> {
11        self.indicator_source_request.take()
12    }
13
14    /// The first visible output with a value at a displayed bar index. Price-based charts
15    /// read element study values, as the legend does. The output number is stable within
16    /// the study and can be used by host alert services.
17    pub fn indicator_value_at(&self, id: SeriesId, index: usize) -> Option<(usize, f64)> {
18        let at = self.study_ids.iter().position(|study| *study == id)?;
19        super::menu_value(
20            self.rendered_studies().indicators().get(at)?.as_ref(),
21            index,
22        )
23    }
24
25    /// Formats a displayed study value with the presentation used by the chart.
26    pub fn indicator_value_text(&self, id: SeriesId, value: f64) -> Option<String> {
27        let at = self.study_ids.iter().position(|study| *study == id)?;
28        Some(
29            self.rendered_studies()
30                .indicators()
31                .get(at)?
32                .format_value(value),
33        )
34    }
35
36    /// Values of visible study plots at a rendered bar index, with legend names and precision.
37    /// Gaps remain `None`; price-based charts use studies calculated from element OHLC.
38    /// Call after [`Self::show`] to use refreshed studies.
39    pub fn study_values_at(&self, index: usize) -> Vec<StudyValue> {
40        self.study_rows_at(index)
41            .into_iter()
42            .map(|(_, row)| row)
43            .collect()
44    }
45
46    /// [`Self::study_values_at`] with the id of each row's study.
47    pub(super) fn study_rows_at(&self, index: usize) -> Vec<(SeriesId, StudyValue)> {
48        if index >= self.state.shown().len() {
49            return Vec::new();
50        }
51        let mut rows = Vec::new();
52        for (&id, study) in self
53            .study_ids
54            .iter()
55            .zip(self.rendered_studies().indicators())
56            .filter(|(_, study)| study.is_visible())
57        {
58            let names = study.line_names();
59            let colors = study.colors();
60            for line in (0..study.line_count()).filter(|line| study.plot_style(*line).visible) {
61                let row = StudyValue {
62                    formatted: study
63                        .plot_value(line, index)
64                        .map(|value| study.format_plot_value(line, value)),
65                    study: study.name().to_owned(),
66                    format: study.value_format(),
67                    plot: names.get(line).cloned().unwrap_or_default(),
68                    value: study.plot_value(line, index),
69                    precision: study
70                        .precision()
71                        .unwrap_or_else(|| match study.value_format() {
72                            crate::scales::StudyFormat::Price => self
73                                .instrument()
74                                .and_then(crate::scales::instrument_price_precision)
75                                .map_or(2, usize::from),
76                            crate::scales::StudyFormat::Fixed(decimals) => decimals.min(12),
77                            crate::scales::StudyFormat::Volume
78                                if study
79                                    .plot_value(line, index)
80                                    .is_some_and(|value| value.abs() < 1_000.0) =>
81                            {
82                                0
83                            }
84                            _ => 2,
85                        }),
86                    color: colors.get(line).copied().unwrap_or(self.config.text_color),
87                };
88                rows.push((id, row));
89            }
90        }
91        rows
92    }
93}
94
95#[cfg(all(test, feature = "studies"))]
96mod tests {
97    use super::*;
98    use crate::model::{Bar, BarData, ChartType, ChartTypeParams};
99    use crate::widget::test_frames::frame;
100
101    #[test]
102    fn menu_values_follow_displayed_elements() {
103        let bars = [100.0, 130.0, 160.0]
104            .into_iter()
105            .enumerate()
106            .map(|(index, close)| {
107                Bar::from_value(
108                    chrono::DateTime::UNIX_EPOCH + chrono::Duration::minutes(index as i64),
109                    close,
110                )
111            })
112            .collect();
113        let mut chart = Chart::new(BarData::from_bars(bars));
114        let id = chart.add_indicator(Box::new(crate::studies::SMA::new(3)));
115        chart.set_chart_type(ChartType::Renko);
116        chart.set_chart_type_params(ChartTypeParams {
117            renko_brick_size: 10.0,
118            ..Default::default()
119        });
120        frame(&egui::Context::default(), &mut chart, 0.0, vec![]);
121        // Six rising bricks close at 110, 120, 130, 140, 150, 160. The third brick's
122        // SMA(3) is (110+120+130)/3 = 120; the third source bar's SMA would be 130.
123        assert_eq!(chart.indicator_value_at(id, 2), Some((0, 120.0)));
124        assert_eq!(
125            chart.indicator_value_text(id, 120.0).as_deref(),
126            Some("120.00")
127        );
128        assert_eq!(chart.indicator_value_at(id, 0), None);
129        chart.set_volume_formatter(Some(std::sync::Arc::new(
130            crate::scales::CustomPriceFormatter::new(|value| format!("{value:.0} lots")),
131        )));
132        let volume = chart
133            .indicators()
134            .find(|(_, study)| study.is_volume())
135            .unwrap()
136            .0;
137        let values = chart.study_values_at(2);
138        assert!(
139            values
140                .iter()
141                .any(|row| row.study == "Volume" && row.formatted_value().ends_with(" lots"))
142        );
143        assert!(
144            chart
145                .indicator_value_text(volume, 25.0)
146                .unwrap()
147                .ends_with(" lots")
148        );
149    }
150}