uniform_nested_coords_plotters/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use core::iter;
4
5use plotters::coord::{
6    combinators::NestedValue,
7    ranged1d::{AsRangedCoord, DiscreteRanged, NoDefaultFormatting, Ranged, ValueFormatter},
8};
9
10/// Like [`plotters::coord::combinators::NestedValue`], but for
11#[derive(Debug, PartialEq, Eq, Clone)]
12pub enum UniformNestedValue<C, V> {
13    /// Caregory value - outer coordinate system only!
14    ///
15    /// Primarily used for rendering key points, and avoids issues when the inner coordinate system
16    /// might not produce key points in the correct location.
17    Category(C),
18    /// Exact value - typically, this is what you'd actually use when providing values.
19    Value(C, V),
20}
21
22impl<C, V> UniformNestedValue<C, V> {
23    /// Obtain the category of this uniform nested value.
24    ///
25    /// Analogous to [`plotters::coord::combinators::NestedValue::category`]
26    pub fn category(&self) -> &C {
27        match self {
28            UniformNestedValue::Category(c) => c,
29            UniformNestedValue::Value(c, _) => c,
30        }
31    }
32
33    /// Obtain the nested value of this, if it is a value that actually refers to a single,
34    /// concrete nested value.
35    ///
36    /// Analogous to [`plotters::coord::combinators::NestedValue::nested_value`]
37    pub fn nested_value(&self) -> Option<&V> {
38        match self {
39            UniformNestedValue::Category(_) => None,
40            UniformNestedValue::Value(_, v) => Some(v),
41        }
42    }
43
44    /// Turn this into a tuple, if it is [`UniformNestedValue::Value`]
45    pub fn maybe_into_tuple(self) -> Option<(C, V)> {
46        match self {
47            UniformNestedValue::Category(_) => None,
48            UniformNestedValue::Value(c, v) => Some((c, v)),
49        }
50    }
51}
52
53impl<C, V> From<(C, V)> for UniformNestedValue<C, V> {
54    #[inline]
55    fn from((c, v): (C, V)) -> Self {
56        Self::Value(c, v)
57    }
58}
59
60// A significant amount of code is taken from [`plotters::coord::combinators::NestedRange`]
61//
62// They are essentially the same concept after all, just one of them does not have individual
63// systems for each outer coordinate.
64/// Like [`plotters::coord::combinators::NestedRange`], except the nested coordinate system is the
65/// same for every item in the outer coordinate system.
66///
67/// That is, each value in the discrete ranged coordinate system has an associated secondary
68/// coordinate system. The value type is [`UniformNestedValue`] - most of the time, you just want
69/// [`UniformNestedValue::Value`] - and a [`From`] implementation for tuples is provided as such.
70///
71/// Because of this uniformity, this can be used very efficiently, even recursively, and even with
72/// large `Primary` coordinate systems - including both as a primary coordinate system itself, or
73/// as a secondary coordinate system. This is because it lacks the need to scan all the
74/// potentially-variadic coordinate systems when doing discrete value indexing.
75///
76/// Important to note here - The coordinate ranges produced by coordinate systems are typically
77/// [inclusive, exclusive) style - they include the lower value and exclude the upper value. This
78/// remains true for this coordinate system. However, when working with both [`UniformNestedRange`]
79/// and [`plotters::coord::combinators::NestedRange`], typically the final value of the *primary*
80/// coordinate system is actually included, and it's only the last value of the secondary
81/// coordinate systems that is excluded. This means that it may extend one-further within the
82/// primary coordinate system than is expected.
83pub struct UniformNestedRange<Primary: DiscreteRanged, Secondary: Ranged> {
84    primary_coordinate_system: Primary,
85    secondary_coordinate_system: Secondary,
86}
87
88// Basically taken directly from the normal implementation.
89impl<PCoordType, SCoordType, P, S> ValueFormatter<NestedValue<PCoordType, SCoordType>>
90    for UniformNestedRange<P, S>
91where
92    P: Ranged<ValueType = PCoordType> + DiscreteRanged,
93    S: Ranged<ValueType = SCoordType>,
94    P: ValueFormatter<PCoordType>,
95    S: ValueFormatter<SCoordType>,
96{
97    fn format(value: &NestedValue<PCoordType, SCoordType>) -> String {
98        match value {
99            NestedValue::Category(c) => P::format(c),
100            NestedValue::Value(_, v) => S::format(v),
101        }
102    }
103}
104
105impl<P: DiscreteRanged, S: Ranged> Ranged for UniformNestedRange<P, S> {
106    type FormatOption = NoDefaultFormatting;
107
108    type ValueType = UniformNestedValue<P::ValueType, S::ValueType>;
109
110    fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 {
111        // This uses the same behaviour as the NestedRange in case the outer coordinate system is
112        // not compatible.
113        let index_in_primary = self
114            .primary_coordinate_system
115            .index_of(value.category())
116            .unwrap_or(0);
117        // This is # of values in the primary discrete coordinate system.
118        let total_primary_buckets = self.primary_coordinate_system.size();
119
120        // Each bucket is allocated to [secondary start, secondary end), from the [primary value,
121        // primary_value.next) region of the resulting i32 coordinate system.
122        //
123        // This is rounded down, and is also signed.
124        let uniform_real_coordinates_buckets_delta =
125            (limit.1 - limit.0) / total_primary_buckets as i32;
126        let mut residual_extra_buckets_delta = (limit.1 - limit.0) % total_primary_buckets as i32;
127
128        // The output coordinate system delta was negative in this case, so the modulo will be too.
129        // Move it up one full modulo chunk
130        if residual_extra_buckets_delta < 0 {
131            residual_extra_buckets_delta += uniform_real_coordinates_buckets_delta;
132        }
133
134        // Left part of the bucket to construct the final output in.
135        //
136        // The `residual stuff . min` part allocates the non-exactly-divided chunks spread across
137        // the output buckets - adding "one-extra" until the residual indivisible bucket delta is
138        // fully distributed.
139        let secondary_bucket_left = limit.0
140            + uniform_real_coordinates_buckets_delta * index_in_primary as i32
141            + residual_extra_buckets_delta.min(index_in_primary as i32);
142
143        // Right part of the bucket.
144        //
145        // The residual stuff again accounts for the allocation of extra space. This is taken from
146        // the NestedRange implementation as this kind of thing gives me a headache lol, but it
147        // seems to be doing the same thing of allocating exactly enough total extra to fill up the
148        // remaining non-exactly-divisible bucket space evenly across the output coordinate space.
149        let secondary_bucket_right = secondary_bucket_left
150            + uniform_real_coordinates_buckets_delta
151            + if (residual_extra_buckets_delta as usize) < index_in_primary {
152                1
153            } else {
154                0
155            };
156
157        // Perform the mapping of the inner secondary coordinate system. Unlike the original
158        // implementation we don't need to perform any indexing due to uniformity of secondary
159        // coordinate system.
160        match value.nested_value() {
161            Some(v) => self
162                .secondary_coordinate_system
163                .map(v, (secondary_bucket_left, secondary_bucket_right)),
164            // In this case - category only -  map to the midpoint
165            None => (secondary_bucket_left + secondary_bucket_right) / 2,
166        }
167    }
168
169    fn key_points<Hint: plotters::coord::ranged1d::KeyPointHint>(
170        &self,
171        hint: Hint,
172    ) -> Vec<Self::ValueType> {
173        // This is taken mostly directly from the default implementation.
174
175        // Either there are no light points, or we have insufficient points allowed to do anything
176        // with secondary coordinate systems inside the buckets (i.e. not enough to put at least
177        // one non-primary key point there, which requires at least one extra available key point
178        // for every primary axis point).
179        if !hint.weight().allow_light_points()
180            || hint.max_num_points() < self.primary_coordinate_system.size() * 2
181        {
182            self.primary_coordinate_system
183                .key_points(hint)
184                .into_iter()
185                .map(UniformNestedValue::Category)
186                .collect()
187        } else {
188            // extra points we have available for each secondary bucket after allocating them to
189            // the primary category values.
190            //
191            // This automatically discards any "extra" indivisible key points.
192            let per_secondary_size = (hint.max_num_points()
193                - self.primary_coordinate_system.size())
194                / self.primary_coordinate_system.size();
195            self.primary_coordinate_system
196                .values()
197                .enumerate()
198                .flat_map(|(category_idx, value)| {
199                    // The key point at the category value
200                    iter::once(UniformNestedValue::Category(value)).chain(
201                        self.secondary_coordinate_system
202                            .key_points(per_secondary_size)
203                            .into_iter()
204                            .map(move |secondary_value| {
205                                (
206                                    self.primary_coordinate_system
207                                        .from_index(category_idx)
208                                        .expect(
209                                        "Invalid index even though we iterated over allowed values",
210                                    ),
211                                    secondary_value,
212                                )
213                                    .into()
214                            }),
215                    )
216                })
217                .collect()
218        }
219    }
220
221    fn range(&self) -> core::ops::Range<Self::ValueType> {
222        // We have a uniform pair of ranges here.
223        let primary_range = self.primary_coordinate_system.range();
224        let secondary_range = self.secondary_coordinate_system.range();
225
226        (primary_range.start, secondary_range.start).into()
227            ..(primary_range.end, secondary_range.end).into()
228    }
229}
230
231/// Efficient implementation of uniformly-nested [`DiscreteRanged`]. Even in uses which require
232/// very large amounts of indexing operations, this should be significantly more efficient than
233/// [`plotters::coord::combinators::NestedRange`], because of the lack of a need to traverse
234/// linearly through dynamic secondary coordinate systems.
235impl<Primary: DiscreteRanged, Secondary: DiscreteRanged> DiscreteRanged
236    for UniformNestedRange<Primary, Secondary>
237{
238    #[inline]
239    fn size(&self) -> usize {
240        // This coordinate system has the slightly surprising behaviour of including values from
241        // the "end part" of the primary coordinate system. However, this is accounted for just
242        // fine by .size().
243        self.primary_coordinate_system.size() * self.secondary_coordinate_system.size()
244    }
245
246    // We - like the normal implementation - do not allow getting the index of pure-category
247    // [`UniformNestedValue`]
248    fn index_of(&self, value: &Self::ValueType) -> Option<usize> {
249        let primary_index = self.primary_coordinate_system.index_of(value.category())?;
250        let secondary_size = self.secondary_coordinate_system.size();
251        let secondary_index = self
252            .secondary_coordinate_system
253            .index_of(value.nested_value()?)?;
254        Some(secondary_size * primary_index + secondary_index)
255    }
256
257    fn from_index(&self, index: usize) -> Option<Self::ValueType> {
258        let secondary_size = self.secondary_coordinate_system.size();
259        let primary_index = index / secondary_size;
260        let secondary_index = index % secondary_size;
261        let category = self.primary_coordinate_system.from_index(primary_index)?;
262        let value = self
263            .secondary_coordinate_system
264            .from_index(secondary_index)?;
265        Some((category, value).into())
266    }
267}
268
269/// Used to build a uniform-nested coordinate system ([`UniformNestedRange`])
270pub trait BuildUniformNestedCoord: AsRangedCoord
271where
272    Self::CoordDescType: DiscreteRanged,
273{
274    /// Builds a uniform nested coordinate system.
275    ///
276    /// Provide a function to generate the single coordinate system you want to nest uniformly
277    /// inside this outer coordinate system.
278    ///
279    /// Will panic if this coordinate system has a [`DiscreteRanged::size`] of zero.
280    fn uniform_nested_coord_with<Secondary: AsRangedCoord>(
281        self,
282        make_secondary: impl FnOnce() -> Secondary,
283    ) -> UniformNestedRange<Self::CoordDescType, Secondary::CoordDescType> {
284        let primary: Self::CoordDescType = self.into();
285        assert!(primary.size() > 0);
286
287        let secondary: Secondary::CoordDescType = make_secondary().into();
288
289        UniformNestedRange {
290            primary_coordinate_system: primary,
291            secondary_coordinate_system: secondary,
292        }
293    }
294
295    /// Builds a uniform nested coordinate system that has been directly provided without an
296    /// intermediary function call.
297    ///
298    /// Will panic if this coordinate system has a [`DiscreteRanged::size`] of zero
299    fn uniform_nested_coord<Secondary: AsRangedCoord>(
300        self,
301        secondary: Secondary,
302    ) -> UniformNestedRange<Self::CoordDescType, Secondary::CoordDescType> {
303        let primary: Self::CoordDescType = self.into();
304        assert!(primary.size() > 0);
305
306        let secondary: Secondary::CoordDescType = secondary.into();
307        UniformNestedRange {
308            primary_coordinate_system: primary,
309            secondary_coordinate_system: secondary,
310        }
311    }
312}
313
314impl<T: AsRangedCoord> BuildUniformNestedCoord for T where T::CoordDescType: DiscreteRanged {}
315
316/// Some basic tests taken approximately from the same module in
317/// [`plotters::coord::combinators::NestedRange`]
318#[cfg(test)]
319mod test {
320    use super::*;
321
322    #[test]
323    fn test_uniform_nested_coord() {
324        let coord = (0..10).uniform_nested_coord(0..39);
325        let range = coord.range();
326        assert_eq!((0, 0).into()..(10, 39).into(), range);
327        assert_eq!(coord.size(), 11 * 40);
328        // 0th category spans [0 -> 100), as the size of the start range is 11
329        // Midpoint should be 50
330        assert_eq!(coord.map(&UniformNestedValue::Category(0), (0, 1100)), 50);
331        assert_eq!(coord.map(&(0, 0).into(), (0, 1100)), 0);
332        // zero-based:
333        // 2/40 => 2/40 => 1/20
334        // 0 + 1/20 * (1100/11) => 5
335        assert_eq!(coord.map(&(0, 2).into(), (0, 1100)), 5);
336        // (19 + 1)/40 = 0.5
337        // 0.5 * (1100/11) + 5 *(1100/11) => 50 + 500 => 550 -> -1 for 0-based
338        assert_eq!(coord.map(&(5, 19).into(), (0, 1100)), 549);
339
340        // '0'th value is zero, '1'st value is 1, ..., '8'th value is 8
341        assert_eq!(coord.from_index(40 * 3 + 8).unwrap(), (3, 8).into());
342        // 10 * 40 = 400, 39 is 0-based->39th value, => 439
343        assert_eq!(coord.index_of(&(10, 39).into()).unwrap(), 439);
344    }
345}
346
347/// Useful includes suitable for performing `uniform_nested_coords_plotters::prelude::*`
348pub mod prelude {
349    pub use super::{BuildUniformNestedCoord, UniformNestedRange, UniformNestedValue};
350}
351
352// uniform-nested-coords-plotters - NestedRange but uniform
353// Copyright (C) 2024  Matti <mattibryce at protonmail dot com>
354//
355// This program is free software: you can redistribute it and/or modify
356// it under the terms of the GNU General Public License as published by
357// the Free Software Foundation, either version 3 of the License, or
358// (at your option) any later version.
359//
360// This program is distributed in the hope that it will be useful,
361// but WITHOUT ANY WARRANTY; without even the implied warranty of
362// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
363// GNU General Public License for more details.
364//
365// You should have received a copy of the GNU General Public License
366// along with this program.  If not, see <https://www.gnu.org/licenses/>.