mapo/
axis.rs

1// TODO implement toPrecision from javascript - it gives better results.
2use crate::{theme, ticker::Ticker};
3use piet_common::{
4    kurbo::{Line, Point, Rect, Size},
5    Color, Error as PietError, Piet, PietTextLayout, RenderContext, Text, TextAttribute,
6    TextLayout, TextLayoutBuilder,
7};
8use std::{fmt, ops::Deref};
9
10const DEFAULT_LABEL_FONT_SIZE: f64 = 16.;
11
12/// Denotes where the axis will be drawn, relative to the chart area.
13///
14/// This will affect the text direction of labels. You can use a `Direction::Left` axis vertically
15/// by rotating it 90 degress, if this gives you the effect you want.
16#[derive(Debug, Copy, Clone, PartialEq)]
17pub enum LabelPosition {
18    /// above or to the left
19    Before,
20    /// below or to the right
21    After,
22}
23
24// # Plan
25//
26// A scale has ticks and labels. The implementation of a scale will supply all the ticks and labels
27// (with the size in pixels as input). It will then be up to a wrapper to layout the labels and work
28// out how many we can fit (and where they should actually be displayed). For now labels will be
29// String only.
30
31/// Axes must be drawn either vertically or horizontally.
32///
33/// To reverse the direction of the axis, reverse the ticker.
34#[derive(Debug, Clone, Copy)]
35pub enum Direction {
36    Horizontal,
37    Vertical,
38}
39
40/// A struct for retaining text layout information for an axis scale.
41///
42/// This struct knows everything it needs to draw the axis, ticks, and labels.
43///
44/// [matplotlib ticker](https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/ticker.py#L2057)
45/// is a good resource.
46#[derive(Clone)]
47pub struct Axis<T> {
48    /// Whether the axis is vertical or horizontal.
49    direction: Direction,
50    /// Where the labels should be shown. Ticks will be drawn on the opposite side.
51    label_pos: LabelPosition,
52    /// An object that knows where the ticks should be drawn.
53    ticker: T,
54
55    // style
56
57    // /// Axis/mark color
58    label_font_size: f64,
59
60    // retained
61    is_layout_valid: bool,
62    /// How long the axis will be
63    axis_len: f64,
64    /// Our computed text layouts for the tick labels.
65    ///
66    /// This is cached, and invalidated by clearing the vec. This way we
67    /// can re-use the allocation. To see if cache is valid, check its
68    /// length against `ticker.len()`.
69    label_layouts: Vec<Label>,
70    /// Which of the layouts we are actually going to draw.
71    labels_to_draw: Vec<usize>,
72}
73
74impl<T: fmt::Debug> fmt::Debug for Axis<T> {
75    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
76        f.debug_struct("Axis")
77            .field("direction", &self.direction)
78            .field("label_pos", &self.label_pos)
79            .field("is_layout_valid", &self.is_layout_valid)
80            .field("axis_len", &self.axis_len)
81            .field("ticker", &self.ticker)
82            .field("label_layouts", &self.label_layouts)
83            .field("labels_to_draw", &self.labels_to_draw)
84            .finish()
85    }
86}
87
88impl<T: Ticker> Axis<T> {
89    /// Create a new axis.
90    pub fn new(direction: Direction, label_pos: LabelPosition, ticker: T) -> Self {
91        Self {
92            direction,
93            label_pos,
94            ticker,
95            label_font_size: DEFAULT_LABEL_FONT_SIZE,
96
97            is_layout_valid: false,
98            axis_len: 0.,
99            label_layouts: vec![],
100            labels_to_draw: vec![],
101        }
102    }
103
104    pub fn ticker(&self) -> &T {
105        &self.ticker
106    }
107
108    pub fn set_ticker(&mut self, new_ticker: T) {
109        self.ticker = new_ticker;
110        self.is_layout_valid = false;
111    }
112
113    pub fn size(&self) -> Size {
114        self.assert_layout();
115
116        // Find the maximum width and height of any label
117        let max_label_size = self.labels_to_draw().fold(Size::ZERO, |mut size, layout| {
118            let run_size = layout.rect().size();
119            if run_size.width > size.width {
120                size.width = run_size.width;
121            }
122            if run_size.height > size.height {
123                size.height = run_size.height;
124            }
125            size
126        });
127
128        match self.direction {
129            Direction::Horizontal => {
130                Size::new(self.axis_len, max_label_size.height + theme::MARGIN)
131            }
132            Direction::Vertical => Size::new(max_label_size.width + theme::MARGIN, self.axis_len),
133        }
134    }
135
136    /// Call this before draw.
137    pub fn layout(&mut self, axis_len: f64, rc: &mut Piet) -> Result<(), PietError> {
138        self.is_layout_valid = true;
139        self.axis_len = axis_len;
140        self.ticker.layout(axis_len);
141        self.build_label_layouts(rc)?;
142        self.fit_labels();
143        Ok(())
144    }
145
146    /// Draw the layout
147    pub fn draw(&self, rc: &mut Piet) {
148        let Size { width, height } = self.size();
149
150        // ticks
151        for tick in self.ticker.ticks() {
152            let tick_line = match (self.direction, self.label_pos) {
153                (Direction::Vertical, LabelPosition::Before) => {
154                    // left
155                    Line::new((width - 5., tick.pos), (width, tick.pos))
156                }
157                (Direction::Vertical, LabelPosition::After) => {
158                    // right
159                    Line::new((0., tick.pos), (5., tick.pos))
160                }
161                (Direction::Horizontal, LabelPosition::Before) => {
162                    // above
163                    Line::new((tick.pos, height - 5.), (tick.pos, height))
164                }
165                (Direction::Horizontal, LabelPosition::After) => {
166                    // below
167                    Line::new((tick.pos, 0.), (tick.pos, 5.))
168                }
169            };
170            rc.stroke(tick_line, &Color::grey8(80), 1.);
171        }
172
173        // axis line (extend to contain tick at edge)
174        let axis_line = match (self.direction, self.label_pos) {
175            (Direction::Horizontal, LabelPosition::Before) => {
176                Line::new((-1., height), (width + 1., height))
177            }
178            (Direction::Horizontal, LabelPosition::After) => Line::new((-1., 0.), (width + 1., 0.)),
179            (Direction::Vertical, LabelPosition::Before) => {
180                Line::new((width, -1.), (width, height + 1.))
181            }
182            (Direction::Vertical, LabelPosition::After) => Line::new((0., -1.), (0., height + 1.)),
183        };
184        rc.stroke(axis_line, &Color::BLACK, 2.);
185
186        // labels
187        for label in self.labels_to_draw() {
188            rc.draw_text(&label.layout, label.pos);
189        }
190    }
191
192    fn build_label_layouts(&mut self, rc: &mut Piet) -> Result<(), PietError> {
193        self.assert_layout();
194        self.label_layouts.clear();
195
196        if self.ticker.len() == 0 {
197            // nothing to do
198            return Ok(());
199        }
200
201        let text = rc.text();
202        // 2 passes - one to create the layouts and find the largest layout size, second to
203        // position the text.
204        let mut largest = Size::ZERO;
205        for tick in self.ticker.ticks() {
206            let layout = text
207                .new_text_layout(tick.label)
208                .default_attribute(TextAttribute::FontSize(self.label_font_size))
209                .build()?;
210            let size = layout.size();
211            if size.width > largest.width {
212                largest.width = size.width;
213            }
214            if size.height > largest.height {
215                largest.height = size.height;
216            }
217            self.label_layouts.push(Label {
218                layout,
219                pos: Point::ZERO,
220            });
221        }
222
223        // 2nd pass to position labels
224        for (pos, label) in self
225            .ticker
226            .ticks()
227            .map(|tick| tick.pos)
228            .zip(self.label_layouts.iter_mut())
229        {
230            let size = label.layout.size();
231
232            let pos = match self.direction {
233                Direction::Horizontal => {
234                    let x = pos - size.width * 0.5;
235                    let y = match self.label_pos {
236                        // TODO assume all line-heights are the same for now
237                        LabelPosition::Before => 0.,
238                        LabelPosition::After => theme::MARGIN,
239                    };
240                    Point { x, y }
241                }
242                Direction::Vertical => {
243                    let x = match self.label_pos {
244                        // right-align
245                        LabelPosition::Before => largest.width - size.width,
246                        // left-align
247                        LabelPosition::After => theme::MARGIN,
248                    };
249                    let y = pos - size.height * 0.5;
250                    Point { x, y }
251                }
252            };
253            label.pos = pos;
254        }
255        Ok(())
256    }
257
258    /// This function needs to be called every time anything affecting label
259    /// positioning changes.
260    fn fit_labels(&mut self) {
261        // Start by trying to fit in all labels, then keep missing more out until
262        // they will fit
263        let mut step = 1;
264        // the loop will never run iff `self.label_layouts.len() == 0`. The below
265        // divides by 2, rounding up.
266        while step <= (self.label_layouts.len() + 1) / 2 {
267            self.labels_to_draw.clear();
268            // TODO if the remainder is odd, put the gap in the middle, if even, split
269            // it between the ends.
270            self.labels_to_draw
271                .extend((0..self.label_layouts.len()).step_by(step));
272            if !self.do_layouts_overlap() {
273                return;
274            }
275            step += 1;
276        }
277        // If we can't layout anything, then show nothing.
278        println!("can't layout anything");
279        self.labels_to_draw.clear();
280    }
281
282    /// Iterate over only those labels we will be drawing.
283    fn labels_to_draw(&self) -> impl Iterator<Item = &'_ Label> {
284        self.labels_to_draw
285            .iter()
286            .copied()
287            .map(|idx| &self.label_layouts[idx])
288    }
289
290    /// Returns `true` if all the labels selected for drawing will fit without overlapping
291    /// each other.
292    ///
293    /// # Panics
294    ///
295    /// Panics if the label layouts have not been built.
296    fn do_layouts_overlap(&self) -> bool {
297        #[cfg(debug_assertions)]
298        self.assert_layout();
299        // strictly speaking the positions of the labels are different depending on whether we are
300        // before or after the axis, but the relative positions don't change, so we can combine
301        // them.
302        //
303        // It is sufficient to test each label with the one preceeding it.
304        let mut prev_rect: Option<Rect> = None;
305
306        for label in self.labels_to_draw() {
307            let rect = label.rect();
308            if let Some(prev_rect) = prev_rect {
309                if !prev_rect.intersect(rect).is_empty() {
310                    return true;
311                }
312            }
313            prev_rect = Some(rect);
314        }
315        false
316    }
317
318    fn assert_layout(&self) {
319        if !self.is_layout_valid {
320            panic!("layout not called");
321        }
322    }
323}
324
325/// The label's text layout with position information
326#[derive(Clone)]
327struct Label {
328    pos: Point,
329    layout: PietTextLayout,
330}
331
332impl Label {
333    pub fn rect(&self) -> Rect {
334        Rect::from_origin_size(self.pos, self.layout.size())
335    }
336}
337
338impl Deref for Label {
339    type Target = PietTextLayout;
340    fn deref(&self) -> &Self::Target {
341        &self.layout
342    }
343}
344
345impl fmt::Debug for Label {
346    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
347        f.debug_struct("Label")
348            .field("text", &self.layout.text())
349            .field("area", &self.rect())
350            .finish()
351    }
352}