Use the actions-rs actions to set things up
1 |
/*!
|
|
2 |
*Views* are plotlib's way of combining multiple representations into a single plot.
|
|
3 |
It is analogous to a *subplot* in other plotting libraries.
|
|
4 |
|
|
5 |
In essence, a view is a collection of representations along with some metadata describing the
|
|
6 |
extent to plot and information about the axes. It knows how to render itself.
|
|
7 |
*/
|
|
8 |
|
|
9 |
use std; |
|
10 |
use std::f64; |
|
11 |
|
|
12 |
use failure::format_err; |
|
13 |
use svg::Node; |
|
14 |
|
|
15 |
use crate::axis; |
|
16 |
use crate::errors::Result; |
|
17 |
use crate::grid::{Grid, GridType}; |
|
18 |
use crate::repr::{CategoricalRepresentation, ContinuousRepresentation}; |
|
19 |
use crate::svg_render; |
|
20 |
use crate::text_render; |
|
21 |
use crate::utils; |
|
22 |
|
|
23 |
pub trait View { |
|
24 |
fn to_svg(&self, face_width: f64, face_height: f64) -> Result<svg::node::element::Group>; |
|
25 |
fn to_text(&self, face_width: u32, face_height: u32) -> Result<String>; |
|
26 |
fn add_grid(&mut self, grid: Grid); |
|
27 |
fn grid(&self) -> &Option<Grid>; |
|
28 |
}
|
|
29 |
|
|
30 |
/// Standard 1-dimensional view with a continuous x-axis
|
|
31 |
#[derive(Default)]
|
|
32 |
pub struct ContinuousView { |
|
33 |
representations: Vec<Box<dyn ContinuousRepresentation>>, |
|
34 |
x_range: Option<axis::Range>, |
|
35 |
y_range: Option<axis::Range>, |
|
36 |
x_max_ticks: usize, |
|
37 |
y_max_ticks: usize, |
|
38 |
x_label: Option<String>, |
|
39 |
y_label: Option<String>, |
|
40 |
grid: Option<Grid>, |
|
41 |
}
|
|
42 |
|
|
43 |
impl ContinuousView { |
|
44 |
/// Create an empty view |
|
45 | 1 |
pub fn new() -> ContinuousView { |
46 |
ContinuousView { |
|
47 | 1 |
representations: vec![], |
48 |
x_range: None, |
|
49 |
y_range: None, |
|
50 |
x_max_ticks: 6, |
|
51 |
y_max_ticks: 6, |
|
52 |
x_label: None, |
|
53 |
y_label: None, |
|
54 |
grid: None, |
|
55 |
} |
|
56 |
} |
|
57 |
/// Set the maximum number of ticks along the x axis. |
|
58 |
pub fn x_max_ticks(mut self, val: usize) -> Self { |
|
59 |
self.x_max_ticks = val; |
|
60 |
self |
|
61 |
} |
|
62 |
/// Set the maximum number of ticks along the y axis. |
|
63 |
pub fn y_max_ticks(mut self, val: usize) -> Self { |
|
64 |
self.y_max_ticks = val; |
|
65 |
self |
|
66 |
} |
|
67 |
|
|
68 |
/// Add a representation to the view |
|
69 | 1 |
pub fn add<R: ContinuousRepresentation + 'static>(mut self, repr: R) -> Self { |
70 | 1 |
self.representations.push(Box::new(repr)); |
71 | 1 |
self |
72 |
} |
|
73 |
|
|
74 |
/// Set the x range for the view |
|
75 | 1 |
pub fn x_range(mut self, min: f64, max: f64) -> Self { |
76 | 1 |
self.x_range = Some(axis::Range::new(min, max)); |
77 | 1 |
self |
78 |
} |
|
79 |
|
|
80 |
/// Set the y range for the view |
|
81 | 1 |
pub fn y_range(mut self, min: f64, max: f64) -> Self { |
82 | 1 |
self.y_range = Some(axis::Range::new(min, max)); |
83 | 1 |
self |
84 |
} |
|
85 |
|
|
86 |
/// Set the label for the x-axis |
|
87 | 1 |
pub fn x_label<T>(mut self, value: T) -> Self |
88 |
where |
|
89 |
T: Into<String>, |
|
90 |
{ |
|
91 | 1 |
self.x_label = Some(value.into()); |
92 | 1 |
self |
93 |
} |
|
94 |
|
|
95 |
/// Set the label for the y-axis |
|
96 | 1 |
pub fn y_label<T>(mut self, value: T) -> Self |
97 |
where |
|
98 |
T: Into<String>, |
|
99 |
{ |
|
100 | 1 |
self.y_label = Some(value.into()); |
101 | 1 |
self |
102 |
} |
|
103 |
|
|
104 | 1 |
fn default_x_range(&self) -> axis::Range { |
105 | 1 |
let mut x_min = f64::INFINITY; |
106 | 1 |
let mut x_max = f64::NEG_INFINITY; |
107 | 1 |
for repr in &self.representations { |
108 | 1 |
let (this_x_min, this_x_max) = repr.range(0); |
109 | 1 |
x_min = x_min.min(this_x_min); |
110 | 1 |
x_max = x_max.max(this_x_max); |
111 |
} |
|
112 | 1 |
let (x_min, x_max) = utils::pad_range_to_zero(x_min, x_max); |
113 | 1 |
axis::Range::new(x_min, x_max) |
114 |
} |
|
115 |
|
|
116 | 1 |
fn default_y_range(&self) -> axis::Range { |
117 | 1 |
let mut y_min = f64::INFINITY; |
118 | 1 |
let mut y_max = f64::NEG_INFINITY; |
119 | 1 |
for repr in &self.representations { |
120 | 1 |
let (this_y_min, this_y_max) = repr.range(1); |
121 | 1 |
y_min = y_min.min(this_y_min); |
122 | 1 |
y_max = y_max.max(this_y_max); |
123 |
} |
|
124 | 1 |
let (y_min, y_max) = utils::pad_range_to_zero(y_min, y_max); |
125 | 1 |
axis::Range::new(y_min, y_max) |
126 |
} |
|
127 |
|
|
128 | 1 |
fn create_axes(&self) -> Result<(axis::ContinuousAxis, axis::ContinuousAxis)> { |
129 | 1 |
let default_x_range = self.default_x_range(); |
130 | 1 |
let x_range = self.x_range.as_ref().unwrap_or(&default_x_range); |
131 | 1 |
if !x_range.is_valid() { |
132 |
return Err(format_err!( |
|
133 |
"Invalid x_range: {} >= {}. Please specify the x_range manually.", |
|
134 |
x_range.lower, |
|
135 |
x_range.upper |
|
136 |
)); |
|
137 |
} |
|
138 |
|
|
139 | 1 |
let default_y_range = self.default_y_range(); |
140 | 1 |
let y_range = self.y_range.as_ref().unwrap_or(&default_y_range); |
141 | 1 |
if !y_range.is_valid() { |
142 |
return Err(format_err!( |
|
143 |
"Invalid y_range: {} >= {}. Please specify the y_range manually.", |
|
144 |
y_range.lower, |
|
145 |
y_range.upper |
|
146 |
)); |
|
147 |
} |
|
148 |
|
|
149 | 1 |
let x_label: String = self.x_label.clone().unwrap_or_else(|| "".to_string()); |
150 | 1 |
let y_label: String = self.y_label.clone().unwrap_or_else(|| "".to_string()); |
151 |
|
|
152 | 1 |
let x_axis = axis::ContinuousAxis::new(x_range.lower, x_range.upper, self.x_max_ticks) |
153 | 1 |
.label(x_label); |
154 | 1 |
let y_axis = axis::ContinuousAxis::new(y_range.lower, y_range.upper, self.y_max_ticks) |
155 | 1 |
.label(y_label); |
156 |
|
|
157 | 1 |
Ok((x_axis, y_axis)) |
158 |
} |
|
159 |
}
|
|
160 |
|
|
161 |
impl View for ContinuousView { |
|
162 |
/** |
|
163 |
Create an SVG rendering of the view
|
|
164 |
*/
|
|
165 | 1 |
fn to_svg(&self, face_width: f64, face_height: f64) -> Result<svg::node::element::Group> { |
166 | 1 |
let mut view_group = svg::node::element::Group::new(); |
167 |
|
|
168 | 1 |
let (x_axis, y_axis) = self.create_axes()?; |
169 |
|
|
170 | 1 |
let (legend_x, mut legend_y) = (face_width - 100., -face_height); |
171 | 1 |
if let Some(grid) = &self.grid { |
172 |
view_group.append(svg_render::draw_grid( |
|
173 |
GridType::Both(grid), |
|
174 |
face_width, |
|
175 |
face_height, |
|
176 |
)); |
|
177 |
} |
|
178 |
|
|
179 |
// Then, based on those ranges, draw each repr as an SVG |
|
180 | 1 |
for repr in &self.representations { |
181 | 1 |
let repr_group = repr.to_svg(&x_axis, &y_axis, face_width, face_height); |
182 | 1 |
view_group.append(repr_group); |
183 |
|
|
184 | 1 |
if let Some(legend_group) = repr.legend_svg() { |
185 |
view_group.append(legend_group.set( |
|
186 |
"transform", |
|
187 |
format!("translate({}, {})", legend_x, legend_y), |
|
188 |
)); |
|
189 |
legend_y += 18.; |
|
190 |
} |
|
191 |
} |
|
192 |
|
|
193 |
// Add in the axes |
|
194 | 1 |
view_group.append(svg_render::draw_x_axis(&x_axis, face_width)); |
195 | 1 |
view_group.append(svg_render::draw_y_axis(&y_axis, face_height)); |
196 |
|
|
197 | 1 |
Ok(view_group) |
198 |
} |
|
199 |
|
|
200 |
/** |
|
201 |
Create a text rendering of the view
|
|
202 |
*/
|
|
203 |
fn to_text(&self, face_width: u32, face_height: u32) -> Result<String> { |
|
204 |
let (x_axis, y_axis) = self.create_axes()?; |
|
205 |
|
|
206 |
let (y_axis_string, longest_y_label_width) = |
|
207 |
text_render::render_y_axis_strings(&y_axis, face_height); |
|
208 |
|
|
209 |
let (x_axis_string, start_offset) = text_render::render_x_axis_strings(&x_axis, face_width); |
|
210 |
|
|
211 |
let left_gutter_width = std::cmp::max( |
|
212 |
longest_y_label_width as i32 + 3, |
|
213 |
start_offset.wrapping_neg(), |
|
214 |
) as u32; |
|
215 |
|
|
216 |
let view_width = face_width + 1 + left_gutter_width + 1; |
|
217 |
let view_height = face_height + 4; |
|
218 |
|
|
219 |
let blank: Vec<String> = (0..view_height) |
|
220 |
.map(|_| (0..view_width).map(|_| ' ').collect()) |
|
221 |
.collect(); |
|
222 |
let mut view_string = blank.join("\n"); |
|
223 |
|
|
224 |
for repr in &self.representations { |
|
225 |
let face_string = repr.to_text(&x_axis, &y_axis, face_width, face_height); |
|
226 |
view_string = |
|
227 |
text_render::overlay(&view_string, &face_string, left_gutter_width as i32 + 1, 0); |
|
228 |
} |
|
229 |
|
|
230 |
let view_string = text_render::overlay( |
|
231 |
&view_string, |
|
232 |
&y_axis_string, |
|
233 |
left_gutter_width as i32 - 2 - longest_y_label_width, |
|
234 |
0, |
|
235 |
); |
|
236 |
let view_string = text_render::overlay( |
|
237 |
&view_string, |
|
238 |
&x_axis_string, |
|
239 |
left_gutter_width as i32, |
|
240 |
face_height as i32, |
|
241 |
); |
|
242 |
|
|
243 |
Ok(view_string) |
|
244 |
} |
|
245 |
|
|
246 |
fn add_grid(&mut self, grid: Grid) { |
|
247 |
self.grid = Some(grid) |
|
248 |
} |
|
249 |
|
|
250 |
fn grid(&self) -> &Option<Grid> { |
|
251 |
&self.grid |
|
252 |
} |
|
253 |
}
|
|
254 |
|
|
255 |
/// A view with categorical entries along the x-axis and continuous values along the y-axis
|
|
256 |
#[derive(Default)]
|
|
257 |
pub struct CategoricalView { |
|
258 |
representations: Vec<Box<dyn CategoricalRepresentation>>, |
|
259 |
x_range: Option<Vec<String>>, |
|
260 |
y_range: Option<axis::Range>, |
|
261 |
x_label: Option<String>, |
|
262 |
y_label: Option<String>, |
|
263 |
grid: Option<Grid>, |
|
264 |
}
|
|
265 |
|
|
266 |
impl CategoricalView { |
|
267 |
/** |
|
268 |
Create an empty view
|
|
269 |
*/
|
|
270 |
pub fn new() -> CategoricalView { |
|
271 |
CategoricalView { |
|
272 |
representations: vec![], |
|
273 |
x_range: None, |
|
274 |
y_range: None, |
|
275 |
x_label: None, |
|
276 |
y_label: None, |
|
277 |
grid: None, |
|
278 |
} |
|
279 |
} |
|
280 |
|
|
281 |
/** |
|
282 |
Add a representation to the view
|
|
283 |
*/
|
|
284 |
pub fn add<R: CategoricalRepresentation + 'static>(mut self, repr: R) -> Self { |
|
285 |
self.representations.push(Box::new(repr)); |
|
286 |
self |
|
287 |
} |
|
288 |
|
|
289 |
/** |
|
290 |
Set the x range for the view
|
|
291 |
*/
|
|
292 |
pub fn x_ticks(mut self, ticks: &[String]) -> Self { |
|
293 |
self.x_range = Some(ticks.into()); |
|
294 |
self |
|
295 |
} |
|
296 |
|
|
297 |
/** |
|
298 |
Set the y range for the view
|
|
299 |
*/
|
|
300 |
pub fn y_range(mut self, min: f64, max: f64) -> Self { |
|
301 |
self.y_range = Some(axis::Range::new(min, max)); |
|
302 |
self |
|
303 |
} |
|
304 |
|
|
305 |
/** |
|
306 |
Set the label for the x-axis
|
|
307 |
*/
|
|
308 |
pub fn x_label<T>(mut self, value: T) -> Self |
|
309 |
where |
|
310 |
T: Into<String>, |
|
311 |
{ |
|
312 |
self.x_label = Some(value.into()); |
|
313 |
self |
|
314 |
} |
|
315 |
|
|
316 |
/** |
|
317 |
Set the label for the y-axis
|
|
318 |
*/
|
|
319 |
pub fn y_label<T>(mut self, value: T) -> Self |
|
320 |
where |
|
321 |
T: Into<String>, |
|
322 |
{ |
|
323 |
self.y_label = Some(value.into()); |
|
324 |
self |
|
325 |
} |
|
326 |
|
|
327 |
fn default_x_ticks(&self) -> Vec<String> { |
|
328 |
let mut v = vec![]; |
|
329 |
for repr in &self.representations { |
|
330 |
for l in repr.ticks() { |
|
331 |
if !v.contains(&l) { |
|
332 |
v.push(l.clone()); |
|
333 |
} |
|
334 |
} |
|
335 |
} |
|
336 |
v |
|
337 |
} |
|
338 |
|
|
339 |
fn default_y_range(&self) -> axis::Range { |
|
340 |
let mut y_min = f64::INFINITY; |
|
341 |
let mut y_max = f64::NEG_INFINITY; |
|
342 |
for repr in &self.representations { |
|
343 |
let (this_y_min, this_y_max) = repr.range(); |
|
344 |
y_min = y_min.min(this_y_min); |
|
345 |
y_max = y_max.max(this_y_max); |
|
346 |
} |
|
347 |
let buffer = (y_max - y_min) / 10.; |
|
348 |
let y_min = if y_min == 0.0 { y_min } else { y_min - buffer }; |
|
349 |
let y_max = y_max + buffer; |
|
350 |
axis::Range::new(y_min, y_max) |
|
351 |
} |
|
352 |
|
|
353 |
fn create_axes(&self) -> Result<(axis::CategoricalAxis, axis::ContinuousAxis)> { |
|
354 |
let default_x_ticks = self.default_x_ticks(); |
|
355 |
let x_range = self.x_range.as_ref().unwrap_or(&default_x_ticks); |
|
356 |
|
|
357 |
let default_y_range = self.default_y_range(); |
|
358 |
let y_range = self.y_range.as_ref().unwrap_or(&default_y_range); |
|
359 |
|
|
360 |
if !y_range.is_valid() { |
|
361 |
return Err(format_err!("invalid y_range: {:?}", y_range)); |
|
362 |
} |
|
363 |
|
|
364 |
let default_x_label = "".to_string(); |
|
365 |
let x_label: String = self.x_label.clone().unwrap_or(default_x_label); |
|
366 |
|
|
367 |
let default_y_label = "".to_string(); |
|
368 |
let y_label: String = self.y_label.clone().unwrap_or(default_y_label); |
|
369 |
|
|
370 |
let x_axis = axis::CategoricalAxis::new(x_range).label(x_label); |
|
371 |
let y_axis = axis::ContinuousAxis::new(y_range.lower, y_range.upper, 6).label(y_label); |
|
372 |
|
|
373 |
Ok((x_axis, y_axis)) |
|
374 |
} |
|
375 |
}
|
|
376 |
|
|
377 |
impl View for CategoricalView { |
|
378 |
fn to_svg(&self, face_width: f64, face_height: f64) -> Result<svg::node::element::Group> { |
|
379 |
let mut view_group = svg::node::element::Group::new(); |
|
380 |
|
|
381 |
let (x_axis, y_axis) = self.create_axes()?; |
|
382 |
|
|
383 |
if let Some(grid) = &self.grid { |
|
384 |
view_group.append(svg_render::draw_grid( |
|
385 |
GridType::HorizontalOnly(grid), |
|
386 |
face_width, |
|
387 |
face_height, |
|
388 |
)); |
|
389 |
} |
|
390 |
|
|
391 |
// Then, based on those ranges, draw each repr as an SVG |
|
392 |
for repr in &self.representations { |
|
393 |
let repr_group = repr.to_svg(&x_axis, &y_axis, face_width, face_height); |
|
394 |
view_group.append(repr_group); |
|
395 |
} |
|
396 |
|
|
397 |
// Add in the axes |
|
398 |
view_group.append(svg_render::draw_categorical_x_axis(&x_axis, face_width)); |
|
399 |
view_group.append(svg_render::draw_y_axis(&y_axis, face_height)); |
|
400 |
|
|
401 |
Ok(view_group) |
|
402 |
} |
|
403 |
|
|
404 |
fn to_text(&self, _face_width: u32, _face_height: u32) -> Result<String> { |
|
405 |
Ok("".into()) |
|
406 |
} |
|
407 |
|
|
408 |
fn add_grid(&mut self, grid: Grid) { |
|
409 |
self.grid = Some(grid); |
|
410 |
} |
|
411 |
|
|
412 |
fn grid(&self) -> &Option<Grid> { |
|
413 |
&self.grid |
|
414 |
} |
|
415 |
}
|
|
416 |
|
|
417 |
/*pub struct AnyView<'a> {
|
|
418 |
representations: Vec<&'a Representation>,
|
|
419 |
axes: Vec<>,
|
|
420 |
x_range: Option<axis::Range>,
|
|
421 |
y_range: Option<axis::Range>,
|
|
422 |
x_label: Option<String>,
|
|
423 |
y_label: Option<String>,
|
|
424 |
}*/
|
Read our documentation on viewing source code .