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/>.