Skip to main content

densa_chart/
series.rs

1use std::ops::Range;
2
3use crate::Axis;
4
5/// A series of points, in ascending `x` (a chart finds the visible ones by binary search):
6/// a line, or the bars of a bar series. A `y` that is not finite is a gap.
7///
8/// Points out of order do not break anything, but they draw wrong and cost more: each step
9/// back restarts the line, and the step forward after it is drawn across every column it
10/// spans.
11pub trait Points {
12    /// How many points there are.
13    fn len(&self) -> usize;
14
15    /// Whether there are none.
16    fn is_empty(&self) -> bool {
17        self.len() == 0
18    }
19
20    /// The `x` of point `index`: never below the one before, never NaN.
21    fn x(&self, index: usize) -> f64;
22
23    /// The `y` of point `index`.
24    fn y(&self, index: usize) -> f64;
25}
26
27impl Points for [(f64, f64)] {
28    fn len(&self) -> usize {
29        <[_]>::len(self)
30    }
31
32    fn x(&self, index: usize) -> f64 {
33        self[index].0
34    }
35
36    fn y(&self, index: usize) -> f64 {
37        self[index].1
38    }
39}
40
41/// `x` values and `y` values side by side; as long as the shorter.
42impl Points for (&[f64], &[f64]) {
43    fn len(&self) -> usize {
44        self.0.len().min(self.1.len())
45    }
46
47    fn x(&self, index: usize) -> f64 {
48        self.0[index]
49    }
50
51    fn y(&self, index: usize) -> f64 {
52        self.1[index]
53    }
54}
55
56/// One candlestick: a period's first, highest, lowest and last values.
57#[derive(Debug, Clone, Copy, PartialEq)]
58pub struct Candle {
59    /// The first value.
60    pub open: f64,
61    /// The highest value.
62    pub high: f64,
63    /// The lowest value.
64    pub low: f64,
65    /// The last value.
66    pub close: f64,
67}
68
69impl Candle {
70    /// A candle from its four values.
71    pub const fn new(open: f64, high: f64, low: f64, close: f64) -> Self {
72        Self {
73            open,
74            high,
75            low,
76            close,
77        }
78    }
79
80    /// Whether every value is finite (others are not drawn).
81    pub fn is_finite(&self) -> bool {
82        self.open.is_finite()
83            && self.high.is_finite()
84            && self.low.is_finite()
85            && self.close.is_finite()
86    }
87}
88
89/// A series of candlesticks in ascending `x` (each candle's period, by its start or its
90/// middle: the chart centers a candle on its `x`).
91pub trait Candles {
92    /// How many candles there are.
93    fn len(&self) -> usize;
94
95    /// Whether there are none.
96    fn is_empty(&self) -> bool {
97        self.len() == 0
98    }
99
100    /// The `x` of candle `index`: never below the one before, never NaN.
101    fn x(&self, index: usize) -> f64;
102
103    /// Candle `index`.
104    fn candle(&self, index: usize) -> Candle;
105}
106
107impl Candles for [(f64, Candle)] {
108    fn len(&self) -> usize {
109        <[_]>::len(self)
110    }
111
112    fn x(&self, index: usize) -> f64 {
113        self[index].0
114    }
115
116    fn candle(&self, index: usize) -> Candle {
117        self[index].1
118    }
119}
120
121/// The indices whose `x` lies within `x` (ends included), found by binary search.
122pub fn points_within<P: Points + ?Sized>(points: &P, x: &Axis) -> Range<usize> {
123    index_range(points.len(), |index| points.x(index), x)
124}
125
126/// The indices of the candles whose `x` lies within `x` (ends included).
127pub fn candles_within<C: Candles + ?Sized>(candles: &C, x: &Axis) -> Range<usize> {
128    index_range(candles.len(), |index| candles.x(index), x)
129}
130
131/// The lowest and highest finite `y` among `range` of `points` (the part of it within
132/// them), or `None` if none is finite.
133pub fn y_extent<P: Points + ?Sized>(points: &P, range: Range<usize>) -> Option<(f64, f64)> {
134    let range = range.start.min(points.len())..range.end.min(points.len());
135    extent(range.map(|index| points.y(index)).map(|y| (y, y)))
136}
137
138/// The lowest low and the highest high among `range` of `candles` (the part of it within
139/// them; finite candles only), or `None` if there are none.
140pub fn candle_extent<C: Candles + ?Sized>(candles: &C, range: Range<usize>) -> Option<(f64, f64)> {
141    let range = range.start.min(candles.len())..range.end.min(candles.len());
142    extent(
143        range
144            .map(|index| candles.candle(index))
145            .filter(Candle::is_finite)
146            .map(|candle| (candle.low, candle.high)),
147    )
148}
149
150fn extent(values: impl Iterator<Item = (f64, f64)>) -> Option<(f64, f64)> {
151    values
152        .filter(|(low, high)| low.is_finite() && high.is_finite())
153        .fold(None, |seen, (low, high)| match seen {
154            None => Some((low, high)),
155            Some((min, max)) => Some((low.min(min), high.max(max))),
156        })
157}
158
159/// The indices `0..len` whose `x` is within `axis`, for ascending `x`.
160fn index_range(len: usize, x: impl Fn(usize) -> f64, axis: &Axis) -> Range<usize> {
161    let start = partition_point(len, |index| x(index) < axis.start());
162    let end = partition_point(len, |index| x(index) <= axis.end());
163    start..end.max(start)
164}
165
166/// The first index of `0..len` for which `before` is false (`before` holds for a prefix).
167fn partition_point(len: usize, before: impl Fn(usize) -> bool) -> usize {
168    let (mut low, mut high) = (0, len);
169    while low < high {
170        let middle = low + (high - low) / 2;
171        if before(middle) {
172            low = middle + 1;
173        } else {
174            high = middle;
175        }
176    }
177    low
178}
179
180/// `range` grown by one index each way, within `0..len`: the neighbours just outside a
181/// plot, so lines run on to its edges.
182pub(crate) fn with_neighbours(range: Range<usize>, len: usize) -> Range<usize> {
183    range.start.saturating_sub(1)..range.end.saturating_add(1).min(len)
184}