1use std::fmt::Debug;
2
3use plotters::coord::ranged1d::AsRangedCoord;
4use plotters::coord::Shift;
5use plotters::prelude::*;
6use plotters::style::FontError;
7
8const INDEX_TOP: usize = 0;
9const INDEX_BOTTOM: usize = 1;
10const INDEX_LEFT: usize = 2;
11const INDEX_RIGHT: usize = 3;
12
13type DrawingResult<T, DB> = Result<T, DrawingAreaErrorKind<<DB as DrawingBackend>::ErrorType>>;
14
15type ChartContext2d<'a, DB, X, Y> = ChartContext<
16 'a,
17 DB,
18 Cartesian2d<<X as AsRangedCoord>::CoordDescType, <Y as AsRangedCoord>::CoordDescType>,
19>;
20
21#[derive(Clone)]
23pub struct ChartLayout<'a> {
24 title_height: u32,
25 title_content: Option<(String, TextStyle<'a>, u32)>,
26 margin: [u32; 4],
27 label_area_size: [u32; 4],
28}
29
30impl<'a> Debug for ChartLayout<'a> {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 f.debug_struct("ChartLayout")
33 .field("title_height", &self.title_height)
34 .field(
35 "title_content",
36 &self.title_content.as_ref().map(|(t, _, _)| t),
37 )
38 .field("margin", &self.margin)
39 .field("label_area_size", &self.label_area_size)
40 .finish()
41 }
42}
43
44fn estimate_text_size(text: &str, font: &FontDesc) -> Result<(u32, u32), FontError> {
45 let text_layout = font.layout_box(text)?;
46 Ok((
47 ((text_layout.1).0 - (text_layout.0).0) as u32,
48 ((text_layout.1).1 - (text_layout.0).1) as u32,
49 ))
50}
51
52impl<'a> ChartLayout<'a> {
53 pub fn new() -> Self {
54 Self {
55 label_area_size: [0; 4],
56 title_height: 0,
57 title_content: None,
58 margin: [0; 4],
59 }
60 }
61
62 pub fn set_all_label_area_size(
63 &mut self,
64 top: u32,
65 bottom: u32,
66 left: u32,
67 right: u32,
68 ) -> &mut Self {
69 self.label_area_size = [top, bottom, left, right];
70 self
71 }
72
73 pub fn x_label_area_size(&mut self, size: u32) -> &mut Self {
74 self.label_area_size[INDEX_BOTTOM] = size;
75 self
76 }
77
78 pub fn y_label_area_size(&mut self, size: u32) -> &mut Self {
79 self.label_area_size[INDEX_LEFT] = size;
80 self
81 }
82
83 pub fn top_x_label_area_size(&mut self, size: u32) -> &mut Self {
84 self.label_area_size[INDEX_TOP] = size;
85 self
86 }
87
88 pub fn right_y_label_area_size(&mut self, size: u32) -> &mut Self {
89 self.label_area_size[INDEX_RIGHT] = size;
90 self
91 }
92
93 pub fn set_all_margin(&mut self, top: u32, bottom: u32, left: u32, right: u32) -> &mut Self {
94 self.margin = [top, bottom, left, right];
95 self
96 }
97
98 pub fn margin(&mut self, size: u32) -> &mut Self {
99 self.margin = [size, size, size, size];
100 self
101 }
102
103 pub fn margin_top(&mut self, size: u32) -> &mut Self {
104 self.margin[INDEX_TOP] = size;
105 self
106 }
107
108 pub fn margin_bottom(&mut self, size: u32) -> &mut Self {
109 self.margin[INDEX_BOTTOM] = size;
110 self
111 }
112
113 pub fn margin_left(&mut self, size: u32) -> &mut Self {
114 self.margin[INDEX_LEFT] = size;
115 self
116 }
117
118 pub fn margin_right(&mut self, size: u32) -> &mut Self {
119 self.margin[INDEX_RIGHT] = size;
120 self
121 }
122
123 pub fn no_caption(&mut self) -> &mut Self {
125 self.title_height = 0;
126 self.title_content = None;
127 self
128 }
129
130 pub fn caption(
132 &mut self,
133 text: impl Into<String>,
134 font: impl Into<FontDesc<'a>>,
135 ) -> Result<&mut Self, FontError> {
136 let text: String = text.into();
137 let font: FontDesc = font.into();
138 let (_, text_h) = estimate_text_size(&text, &font)?;
139 let style: TextStyle = font.into();
140 let y_padding = (text_h / 2).min(5);
141 self.title_height = y_padding * 2 + text_h;
142 self.title_content = Some((text, style, y_padding));
143 Ok(self)
144 }
145
146 pub fn replace_caption(&mut self, text: impl Into<String>) -> &mut Self {
150 let text: String = text.into();
151 if let Some((_, style, y_padding)) = self.title_content.take() {
152 self.title_content = Some((text, style, y_padding));
153 }
154 self
155 }
156
157 fn additional_sizes(&self) -> (u32, u32) {
158 let [m_top, m_bottom, m_left, m_right] = self.margin;
159 let [l_top, l_bottom, l_left, l_right] = self.label_area_size;
160 let width = m_left + m_right + l_left + l_right;
161 let height = self.title_height + m_top + m_bottom + l_top + l_bottom;
162 (width, height)
163 }
164
165 pub fn desired_image_size(&self, plot_size: (u32, u32)) -> (u32, u32) {
169 let additional = self.additional_sizes();
170 (plot_size.0 + additional.0, plot_size.1 + additional.1)
171 }
172
173 pub fn desired_image_height_from_width(&self, image_width: u32, aspect_ratio: f64) -> u32 {
177 let additional = self.additional_sizes();
178 if image_width < additional.0 {
179 additional.1
180 } else {
181 ((image_width - additional.0) as f64 * aspect_ratio) as u32 + additional.1
182 }
183 }
184
185 pub fn bind<'b, DB>(
187 &self,
188 root_area: &'b DrawingArea<DB, Shift>,
189 ) -> DrawingResult<ChartLayoutBuilder<'b, DB>, DB>
190 where
191 'a: 'b,
192 DB: DrawingBackend,
193 {
194 use plotters::style::text_anchor::{HPos, Pos, VPos};
195
196 let title_area_height = self.title_height;
197 let main_area = if title_area_height > 0 {
198 let (title_area, main_area) = root_area.split_vertically(title_area_height);
199 if let Some((text, style, y_padding)) = &self.title_content {
200 let dim = title_area.dim_in_pixel();
201 let x_padding = dim.0 / 2;
202 let style = &style.pos(Pos::new(HPos::Center, VPos::Top));
203 title_area.draw_text(text, style, (x_padding as i32, *y_padding as i32))?;
204 main_area
205 } else {
206 main_area
207 }
208 } else {
209 root_area.clone()
210 };
211 Ok(ChartLayoutBuilder {
212 layout: self.clone(),
213 main_area,
214 })
215 }
216}
217
218impl<'a> Default for ChartLayout<'a> {
219 fn default() -> Self {
220 Self::new()
221 }
222}
223
224pub struct ChartLayoutBuilder<'a, DB: DrawingBackend> {
225 layout: ChartLayout<'a>,
226 main_area: DrawingArea<DB, Shift>,
227}
228
229impl<'a, DB: DrawingBackend> ChartLayoutBuilder<'a, DB> {
230 pub fn estimate_plot_area_size(&self) -> (u32, u32) {
234 let [m_top, m_bottom, m_left, m_right] = self.layout.margin;
235 let [l_top, l_bottom, l_left, l_right] = self.layout.label_area_size;
236 let (image_width, image_height) = self.main_area.dim_in_pixel();
238 let plot_width = image_width - (m_left + m_right + l_left + l_right);
239 let plot_height = image_height - (m_top + m_bottom + l_top + l_bottom);
240 (plot_width, plot_height)
241 }
242
243 pub fn build_cartesian_2d<X: AsRangedCoord, Y: AsRangedCoord>(
244 &self,
245 x_spec: X,
246 y_spec: Y,
247 ) -> DrawingResult<ChartContext2d<DB, X, Y>, DB> {
248 let [m_top, m_bottom, m_left, m_right] = self.layout.margin;
249 let [l_top, l_bottom, l_left, l_right] = self.layout.label_area_size;
250
251 let mut builder = ChartBuilder::on(&self.main_area);
252
253 builder
254 .margin_top(m_top)
255 .margin_bottom(m_bottom)
256 .margin_left(m_left)
257 .margin_right(m_right)
258 .set_label_area_size(LabelAreaPosition::Top, l_top)
259 .set_label_area_size(LabelAreaPosition::Bottom, l_bottom)
260 .set_label_area_size(LabelAreaPosition::Left, l_left)
261 .set_label_area_size(LabelAreaPosition::Right, l_right);
262
263 builder.build_cartesian_2d(x_spec, y_spec)
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use std::error::Error;
270 use std::ops::Range;
271
272 use plotters::backend::RGBPixel;
273 use plotters::prelude::*;
274
275 use super::ChartLayout;
276
277 #[test]
278 fn size_estimation() -> Result<(), Box<dyn Error>> {
279 let x_spec = 0.0..2.0;
280 let y_spec = -1.0..2.0;
281 let plot_size = (200, 350);
282 let mut layout = ChartLayout::new();
283
284 for i in 0..0x200 {
285 layout.set_all_margin(
286 if i & 0x1 == 0 { 5 } else { 0 },
287 if i & 0x2 == 0 { 10 } else { 0 },
288 if i & 0x4 == 0 { 12 } else { 0 },
289 if i & 0x8 == 0 { 15 } else { 0 },
290 );
291 layout.set_all_label_area_size(
292 if i & 0x10 == 0 { 20 } else { 0 },
293 if i & 0x20 == 0 { 25 } else { 0 },
294 if i & 0x40 == 0 { 30 } else { 0 },
295 if i & 0x80 == 0 { 32 } else { 0 },
296 );
297 if i & 0x100 == 0 {
298 layout.caption("Test Title", ("sans-serif", 20))?;
299 } else {
300 layout.no_caption();
301 }
302 bmp2d_size_estimation(&layout, plot_size, x_spec.clone(), y_spec.clone())?;
303 }
304
305 Ok(())
306 }
307
308 fn bmp2d_size_estimation(
309 layout: &ChartLayout,
310 plot_size: (u32, u32),
311 x_spec: Range<f64>,
312 y_spec: Range<f64>,
313 ) -> Result<(), Box<dyn Error>> {
314 let image_size = layout.desired_image_size(plot_size);
315
316 let mut buf = vec![0u8; (3 * image_size.0 * image_size.1) as usize];
317 let backend: BitMapBackend<RGBPixel> =
318 BitMapBackend::with_buffer_and_format(&mut buf, image_size)?;
319 let root_area = backend.into_drawing_area();
320
321 let builder = layout.bind(&root_area)?;
322 let estimated_plot_size = builder.estimate_plot_area_size();
323 assert_eq!(
324 plot_size, estimated_plot_size,
325 "wrong estimation; layout = {layout:?}, image_size = {image_size:?}"
326 );
327
328 let chart = builder.build_cartesian_2d(x_spec, y_spec)?;
329 let actual_size = chart.plotting_area().dim_in_pixel();
330
331 assert_eq!(
332 plot_size, actual_size,
333 "wrong actual size, layout = {layout:?}, image_size = {image_size:?}"
334 );
335 Ok(())
336 }
337}