1use std::borrow::Borrow;
2use std::collections::HashMap;
3
4use std::str::FromStr;
5
6use svg::node::element::{Group, Rectangle, Text};
7use svg::node::Text as nodeText;
8use svg::Document;
9
10use crate::blobdir::FieldMeta;
11use crate::cli::Shape;
12use crate::plot::component::LegendAlignment;
13use crate::utils::{max_float, min_float, scale_floats};
14use crate::{blobdir, cli, plot};
15
16use plot::category::Category;
17
18use super::axis::{AxisName, AxisOptions, ChartAxes, Position, Scale, TickOptions};
19use super::chart::{Chart, Dimensions, TopRightBottomLeft};
20use super::component::{font_family, legend_group, LegendEntry};
21use super::data::{Bin, HistogramData, Reducer, ScatterData, ScatterPoint};
22use super::{GridSize, ShowLegend};
23
24#[derive(Clone, Debug)]
25pub struct BlobData {
26 pub x: Vec<f64>,
27 pub y: Vec<f64>,
28 pub z: Vec<f64>,
29 pub cat: Vec<Option<usize>>,
30 pub cat_order: Vec<Category>,
31 pub title: Option<String>,
32}
33
34#[derive(Clone, Debug)]
35pub struct BlobDimensions {
36 pub height: f64,
37 pub width: f64,
38 pub margin: TopRightBottomLeft,
39 pub padding: TopRightBottomLeft,
40 pub hist_height: f64,
41 pub hist_width: f64,
42}
43
44impl Default for BlobDimensions {
45 fn default() -> BlobDimensions {
46 let dimensions = Dimensions {
47 ..Default::default()
48 };
49 BlobDimensions {
50 height: dimensions.height,
51 width: dimensions.width,
52 margin: dimensions.margin,
53 padding: dimensions.padding,
54 hist_height: 250.0,
55 hist_width: 250.0,
56 }
57 }
58}
59
60fn scale_values(data: &Vec<f64>, meta: &AxisOptions) -> Vec<f64> {
61 let mut scaled = vec![];
62 for value in data {
63 scaled.push(scale_floats(
64 *value,
65 &meta.domain,
66 &meta.range,
67 &meta.scale,
68 meta.clamp,
69 ));
70 }
71 scaled
72}
73
74pub fn bin_axis(
75 scatter_data: &ScatterData,
76 blob_data: &BlobData,
77 axis: AxisName,
78 options: &cli::PlotOptions,
79) -> (Vec<Vec<f64>>, f64) {
80 let range = match axis {
81 AxisName::X => scatter_data.x.range,
82 AxisName::Y => scatter_data.y.range,
83 AxisName::Z => scatter_data.z.range,
84 _ => [0.0, 100.0],
85 };
86 let bin_size = (range[1] - range[0]) / options.resolution as f64;
87 let mut binned = vec![vec![0.0; options.resolution]; options.cat_count];
88 let mut counts = vec![vec![0.0; options.resolution]; options.cat_count];
89 let mut max_bin = 0.0;
90 for point in scatter_data.points.iter() {
91 let cat_index = point.cat_index;
92 let mut bin = match axis {
93 AxisName::X => ((point.x - range[0]) / bin_size).floor() as usize,
94 AxisName::Y => ((point.y - range[0]) / bin_size).floor() as usize,
95 AxisName::Z => ((point.z - range[0]) / bin_size).floor() as usize,
96 _ => 0,
97 };
98 if bin == options.resolution {
99 bin -= 1;
100 }
101 match options.reducer_function {
102 Reducer::Sum => binned[cat_index][bin] += blob_data.z[point.data_index],
103 Reducer::Max => {
104 binned[cat_index][bin] =
105 max_float(binned[cat_index][bin], blob_data.z[point.data_index])
106 }
107 Reducer::Min => {
108 binned[cat_index][bin] = if binned[cat_index][bin] == 0.0 {
109 blob_data.z[point.data_index]
110 } else {
111 min_float(binned[cat_index][bin], blob_data.z[point.data_index])
112 }
113 }
114 Reducer::Count => binned[cat_index][bin] += 1.0,
115 Reducer::Mean => {
116 binned[cat_index][bin] += blob_data.z[point.data_index];
117 counts[cat_index][bin] += 1.0
118 }
119 };
120 max_bin = max_float(max_bin, binned[cat_index][bin]);
121 }
122 match options.reducer_function {
123 Reducer::Mean => {
124 max_bin = 0.0;
125 for (cat_index, bins) in binned.clone().iter().enumerate() {
126 for (bin, _) in bins.iter().enumerate() {
127 if counts[cat_index][bin] > 0.0 {
128 binned[cat_index][bin] /= counts[cat_index][bin];
129 max_bin = max_float(max_bin, binned[cat_index][bin]);
130 }
131 }
132 }
133 }
134 Reducer::Min => {
135 max_bin = 0.0;
136 for (cat_index, bins) in binned.clone().iter().enumerate() {
137 for (bin, _) in bins.iter().enumerate() {
138 max_bin = max_float(max_bin, binned[cat_index][bin]);
139 }
140 }
141 }
142 _ => (),
143 }
144 (binned, max_bin)
145}
146
147pub fn axis_hist(
148 binned: Vec<Vec<f64>>,
149 blob_data: &BlobData,
150 dimensions: &BlobDimensions,
151 max_bin: f64,
152 axis: AxisName,
153 options: &cli::PlotOptions,
154) -> Vec<HistogramData> {
155 let domain = [0.0, max_bin];
156 let (width, range) = match axis {
157 AxisName::X => (dimensions.width, [0.0, dimensions.hist_height]),
158 _ => (dimensions.height, [0.0, dimensions.hist_width]),
159 };
160 let cat_order = blob_data.cat_order.clone();
161 let bin_width = width / options.resolution as f64;
162 let mut histograms = vec![
163 HistogramData {
164 max_bin,
165 width,
166 ..Default::default()
167 };
168 cat_order.len() - 1
169 ];
170 for (j, cat) in cat_order.iter().enumerate() {
171 if j == 0 {
172 continue;
173 }
174 let i = j - 1;
175 histograms[i] = HistogramData {
176 bins: binned[i]
177 .iter()
178 .map(|value| Bin {
179 height: scale_floats(*value, &domain, &range, &Scale::LINEAR, None),
180 width: bin_width,
181 value: *value,
182 })
183 .collect(),
184 max_bin: scale_floats(max_bin, &domain, &range, &Scale::LINEAR, None),
185 axis: axis.clone(),
186 category: Some(cat.clone()),
187 ..histograms[i]
188 }
189 }
190 histograms
191}
192
193pub fn bin_axes(
194 scatter_data: &ScatterData,
195 blob_data: &BlobData,
196 dimensions: &BlobDimensions,
197 options: &cli::PlotOptions,
198) -> (Vec<HistogramData>, Vec<HistogramData>, f64) {
199 let (x_binned, x_max) = bin_axis(scatter_data, blob_data, AxisName::X, options);
200 let (y_binned, y_max) = bin_axis(scatter_data, blob_data, AxisName::Y, options);
201 let mut max_bin = max_float(x_max, y_max);
202 if options.hist_height.is_some() {
203 max_bin = max_float(max_bin, options.hist_height.unwrap() as f64)
204 }
205 let x_histograms = axis_hist(
206 x_binned,
207 blob_data,
208 dimensions,
209 max_bin,
210 AxisName::X,
211 options,
212 );
213 let y_histograms = axis_hist(
214 y_binned,
215 blob_data,
216 dimensions,
217 max_bin,
218 AxisName::Y,
219 options,
220 );
221 (x_histograms, y_histograms, max_bin)
222}
223
224fn set_domain(
225 field_meta: &FieldMeta,
226 limit_string: Option<String>,
227 limit_arr: Option<[f64; 2]>,
228 limit_clamp: Option<f64>,
229) -> ([f64; 2], Option<f64>) {
230 let clamp = limit_clamp.unwrap_or(0.1);
231 let mut domain = field_meta.range.unwrap();
232 if limit_string.is_some() {
233 if let Some((min_value, max_value)) = limit_string.clone().unwrap().split_once(",") {
234 if !min_value.is_empty() {
235 domain[0] = min_value.parse::<f64>().unwrap();
236 }
237 if !max_value.is_empty() {
238 domain[1] = max_value.parse::<f64>().unwrap();
239 }
240 }
241 } else if limit_arr.is_some() {
242 domain = limit_arr.unwrap();
243 }
244 let clamp_value = if field_meta.clamp.is_some() {
245 domain[0] = field_meta.range.unwrap()[0];
246 field_meta.clamp
247 } else if field_meta.range.unwrap()[0] == 0.0 && field_meta.scale.clone().unwrap() == "scaleLog"
248 {
249 domain[0] = clamp;
250 Some(clamp)
251 } else {
252 None
253 };
254 if domain[0] == domain[1] {
255 if domain[0] == 0.0 {
256 domain[1] += 0.1;
257 } else {
258 domain[0] /= 0.1;
259 domain[1] *= 0.1;
260 }
261 }
262 (domain, clamp_value)
266}
267
268pub fn blob_points(
269 axes: HashMap<String, String>,
270 blob_data: &BlobData,
271 dimensions: &BlobDimensions,
272 meta: &blobdir::Meta,
273 options: &cli::PlotOptions,
274 limits: Option<HashMap<String, [f64; 2]>>,
275) -> ScatterData {
276 let fields = meta.field_list.clone().unwrap();
277 let x_meta = fields[axes["x"].as_str()].clone();
278 let (x_limit_arr, y_limit_arr) = match limits {
279 Some(limit) => (Some(limit["x"]), Some(limit["y"])),
280 None => (None, None),
281 };
282 let (x_domain, x_clamp) = set_domain(&x_meta, options.x_limit.clone(), x_limit_arr, None);
283 let x_axis = AxisOptions {
284 position: Position::BOTTOM,
285 height: dimensions.height + dimensions.padding.top + dimensions.padding.bottom,
286 label: axes["x"].clone(),
287 padding: [dimensions.padding.left, dimensions.padding.right],
288 offset: dimensions.height + dimensions.padding.top + dimensions.padding.bottom,
289 scale: Scale::from_str(&x_meta.scale.unwrap()).unwrap(),
290 domain: x_domain,
291 range: [0.0, dimensions.width],
292 clamp: x_clamp,
293 ..Default::default()
294 };
295 let x_scaled = scale_values(&blob_data.x, &x_axis);
296
297 let y_meta = fields[axes["y"].as_str()].clone();
298 let (y_domain, y_clamp) = set_domain(&y_meta, options.y_limit.clone(), y_limit_arr, None);
299
300 let y_axis = AxisOptions {
309 position: Position::LEFT,
310 height: dimensions.width + dimensions.padding.right + dimensions.padding.left,
311 label: axes["y"].clone(),
312 padding: [dimensions.padding.bottom, dimensions.padding.top],
313 scale: Scale::from_str(&y_meta.scale.unwrap()).unwrap(),
314 domain: y_domain,
315 range: [dimensions.height, 0.0],
316 clamp: y_clamp,
317 rotate: true,
318 ..Default::default()
319 };
320 let y_scaled = scale_values(&blob_data.y, &y_axis);
321
322 let z_meta = fields[axes["z"].as_str()].clone();
323 let mut z_domain = z_meta.range.unwrap();
324 if z_domain[0] == z_domain[1] {
325 if z_domain[0] == 0.0 {
326 z_domain[1] += 0.1;
327 } else {
328 z_domain[0] /= 2.0;
329 z_domain[1] *= 2.0;
330 }
331 }
332 let z_axis = AxisOptions {
333 label: axes["z"].clone(),
334 scale: options.resolved_scale_function(),
335 domain: z_domain,
336 range: [2.0, 2.0 + dimensions.height / 15.0 * options.scale_factor],
337 ..Default::default()
338 };
339 let z_scaled = scale_values(&blob_data.z, &z_axis);
340 let mut points = vec![];
341 let mut fg_points = vec![];
342 match options.shape {
343 Some(Shape::Grid) => {
344 for (i, x) in x_scaled.iter().enumerate() {
345 if blob_data.cat.len() <= i || blob_data.cat[i].is_none() {
346 points.push(ScatterPoint {
347 x: *x,
348 y: y_scaled[i],
349 z: z_scaled[i] * 1.5,
350 data_index: i,
351 ..Default::default()
352 });
353 continue;
354 }
355 if let Some(cat_index) = blob_data.cat[i] {
356 if blob_data.cat_order.len() < cat_index + 1 {
357 continue;
358 }
359 let cat = blob_data.cat_order[cat_index].clone();
360 let point = ScatterPoint {
361 x: *x,
362 y: y_scaled[i],
363 z: z_scaled[i] * 1.5,
364 label: Some(cat.title.clone()),
365 color: Some(cat.color.clone()),
366 cat_index,
367 data_index: i,
368 };
369 points.push(point.clone());
370 fg_points.push(point.clone());
371 }
372 }
373 points.extend(fg_points);
374 }
375 _ => {
376 let cat_order = blob_data.cat_order.clone();
377 let mut ordered_points = vec![vec![]; cat_order.len() - 1];
378 for (i, cat_index) in blob_data.cat.iter().enumerate() {
380 if let Some(idx) = cat_index {
381 let cat = cat_order[*idx].borrow();
382 ordered_points[*idx - 1].push(ScatterPoint {
383 x: x_scaled[i],
384 y: y_scaled[i],
385 z: z_scaled[i],
386 label: Some(cat.title.clone()),
387 color: Some(cat.color.clone()),
388 cat_index: *idx - 1,
389 data_index: i,
390 })
391 }
392 }
393 for cat_points in ordered_points {
394 points.extend(cat_points);
395 }
396 }
397 }
398 ScatterData {
399 points,
400 x: x_axis,
401 y: y_axis,
402 z: z_axis,
403 categories: blob_data.cat_order.clone(),
404 }
405}
406
407pub fn category_legend_full(categories: Vec<Category>, show_legend: ShowLegend) -> Group {
408 let mut entries = vec![];
409 let title = "".to_string();
410 if let ShowLegend::Full = show_legend {
411 entries.push(LegendEntry {
412 subtitle: Some("[count; span; n50]".to_string()),
413 ..Default::default()
414 })
415 };
416 for (i, cat) in categories.iter().enumerate() {
417 if i == 0 {
418 match show_legend {
419 ShowLegend::Full => (),
420 _ => continue,
421 };
422 }
423 let subtitle = match show_legend {
424 ShowLegend::Compact => None,
425 ShowLegend::Default | ShowLegend::Full => Some(cat.clone().subtitle()),
426 ShowLegend::None => {
427 return legend_group(title, entries, None, 1, LegendAlignment::Start)
428 }
429 };
430 entries.push(LegendEntry {
431 title: cat.title.to_string(),
432 color: Some(cat.color.clone()),
433 subtitle,
434 ..Default::default()
435 });
436 }
437 legend_group(title, entries, None, 1, LegendAlignment::Start)
438}
439
440pub fn plot(
441 blob_dimensions: BlobDimensions,
442 scatter_data: ScatterData,
443 hist_data_x: Vec<HistogramData>,
444 hist_data_y: Vec<HistogramData>,
445 x_max: f64,
446 y_max: f64,
447 options: &cli::PlotOptions,
448) -> Document {
449 let height = blob_dimensions.height
450 + blob_dimensions.hist_height
451 + blob_dimensions.margin.top
452 + blob_dimensions.margin.bottom
453 + blob_dimensions.padding.top
454 + blob_dimensions.padding.bottom;
455
456 let width = blob_dimensions.width
457 + blob_dimensions.hist_width
458 + blob_dimensions.margin.right
459 + blob_dimensions.margin.left
460 + blob_dimensions.padding.right
461 + blob_dimensions.padding.left;
462 let x_opts = scatter_data.x.clone();
463 let y_opts = scatter_data.y.clone();
464
465 let scatter = Chart {
466 axes: ChartAxes {
467 x: Some(x_opts.clone()),
468 y: Some(y_opts.clone()),
469 ..Default::default()
470 },
471 scatter_data: Some(scatter_data.clone()),
472 dimensions: Dimensions {
473 height: blob_dimensions.height,
474 width: blob_dimensions.width,
475 margin: blob_dimensions.margin,
476 padding: blob_dimensions.padding,
477 },
478 ..Default::default()
479 };
480
481 let x_hist = Chart {
482 axes: ChartAxes {
483 x: Some(AxisOptions {
484 label: "".to_string(),
485 offset: blob_dimensions.hist_height,
486 height: blob_dimensions.hist_height,
487 tick_labels: false,
488 ..x_opts.clone()
489 }),
490 y: Some(AxisOptions {
491 position: Position::LEFT,
492 label: "sum length".to_string(),
493 label_offset: 80.0,
494 height: blob_dimensions.width
495 + blob_dimensions.padding.right
496 + blob_dimensions.padding.left,
497 font_size: 25.0,
498 scale: Scale::LINEAR,
499 domain: [0.0, x_max],
500 range: [blob_dimensions.hist_height, 0.0],
501 rotate: true,
502 tick_count: 5,
503 ..Default::default()
504 }),
505 x2: Some(AxisOptions {
506 offset: 0.0,
507 position: Position::TOP,
508 major_ticks: None,
509 minor_ticks: None,
510 ..x_opts.clone()
511 }),
512 y2: Some(AxisOptions {
513 position: Position::RIGHT,
514 offset: blob_dimensions.width
515 + blob_dimensions.padding.right
516 + blob_dimensions.padding.left,
517 scale: Scale::LINEAR,
518 domain: [0.0, x_max],
519 range: [blob_dimensions.hist_height, 0.0],
520 major_ticks: None,
521 minor_ticks: None,
522 ..Default::default()
523 }),
524 ..Default::default()
525 },
526 histogram_data: Some(hist_data_x),
527 dimensions: Dimensions {
528 height: blob_dimensions.hist_height,
529 width: blob_dimensions.width,
530 margin: TopRightBottomLeft {
531 ..Default::default()
532 },
533 padding: TopRightBottomLeft {
534 right: blob_dimensions.padding.right,
535 left: blob_dimensions.padding.left,
536 ..Default::default()
537 },
538 },
539 ..Default::default()
540 };
541
542 let y_hist = Chart {
543 axes: ChartAxes {
544 x: Some(AxisOptions {
545 offset: 0.0,
546 height: blob_dimensions.hist_height,
547 label: "".to_string(),
548 tick_labels: false,
549 ..y_opts.clone()
550 }),
551 y: Some(AxisOptions {
552 position: Position::BOTTOM,
553 height: blob_dimensions.height
554 + blob_dimensions.padding.top
555 + blob_dimensions.padding.bottom,
556 offset: blob_dimensions.height
557 + blob_dimensions.padding.top
558 + blob_dimensions.padding.bottom,
559 label: "sum length".to_string(),
560 label_offset: 80.0,
561 font_size: 25.0,
562 scale: Scale::LINEAR,
563 domain: [0.0, y_max],
564 range: [0.0, blob_dimensions.hist_width],
565 tick_count: 5,
566 rotate: true,
567 ..Default::default()
568 }),
569 x2: Some(AxisOptions {
570 offset: blob_dimensions.hist_width,
571 position: Position::RIGHT,
572 major_ticks: None,
573 minor_ticks: None,
574 label: "".to_string(),
575 ..y_opts.clone()
576 }),
577 y2: Some(AxisOptions {
578 position: Position::TOP,
579 offset: 0.0,
580 scale: Scale::LINEAR,
581 domain: [0.0, y_max],
582 range: [0.0, blob_dimensions.hist_width],
583 major_ticks: None,
584 minor_ticks: None,
585 label: "".to_string(),
586 ..Default::default()
587 }),
588
589 ..Default::default()
590 },
591 histogram_data: Some(hist_data_y),
592 dimensions: Dimensions {
593 height: blob_dimensions.hist_width,
594 width: blob_dimensions.height,
595 margin: TopRightBottomLeft {
596 ..Default::default()
597 },
598 padding: TopRightBottomLeft {
599 top: blob_dimensions.padding.top,
600 bottom: blob_dimensions.padding.bottom,
601 ..Default::default()
602 },
603 },
604 ..Default::default()
605 };
606
607 let legend_x = match options.show_legend {
608 ShowLegend::Compact => width - blob_dimensions.hist_width,
609 _ => width - 185.0,
610 };
611
612 let opacity = 0.6;
613
614 let document = Document::new()
615 .set("viewBox", (0, 0, width, height))
616 .add(
617 Rectangle::new()
618 .set("fill", "#ffffff")
619 .set("stroke", "none")
620 .set("width", width)
621 .set("height", height),
622 )
623 .add(scatter.svg(0.0, 0.0, Some(opacity)).set(
624 "transform",
625 format!(
626 "translate({}, {})",
627 blob_dimensions.margin.left,
628 blob_dimensions.hist_height + blob_dimensions.margin.top
629 ),
630 ))
631 .add(x_hist.svg(0.0, 0.0, Some(opacity)).set(
632 "transform",
633 format!(
634 "translate({}, {})",
635 blob_dimensions.margin.left, blob_dimensions.margin.top
636 ),
637 ))
638 .add(y_hist.svg(0.0, 0.0, Some(opacity)).set(
639 "transform",
640 format!(
641 "translate({}, {})",
642 blob_dimensions.margin.left
643 + blob_dimensions.width
644 + blob_dimensions.padding.right
645 + blob_dimensions.padding.left,
646 blob_dimensions.hist_height + blob_dimensions.margin.top
647 ),
648 ))
649 .add(
650 category_legend_full(scatter_data.categories, options.show_legend.clone())
651 .set("transform", format!("translate({}, {})", legend_x, 10.0)),
652 );
653
654 document
655}
656
657pub fn plot_grid(
658 grid_size: GridSize,
659 scatter_data: Vec<ScatterData>,
660 titles: Vec<Option<String>>,
661 labels: (String, String),
662 _options: &cli::PlotOptions,
663) -> Document {
664 let x_label = labels.0;
665 let y_label = labels.1;
666 let height = grid_size.row_height - grid_size.margin.top - grid_size.margin.bottom;
667
668 let mut charts = vec![];
669
670 let offset = grid_size.row_height - grid_size.margin.bottom; let y_range = [
676 grid_size.row_height
677 - grid_size.padding.bottom
678 - grid_size.padding.top
679 - grid_size.margin.bottom,
680 grid_size.margin.top,
681 ];
682
683 let font_size = 20.0;
684 for (i, data) in scatter_data.iter().enumerate() {
687 let col = i / grid_size.num_rows;
688 let row = i % grid_size.num_rows;
689 let x_opts = data.x.clone();
690 let y_opts = data.y.clone();
691 let width = grid_size.col_widths[col] - grid_size.margin.left - grid_size.margin.right;
692
693 let range = [
694 grid_size.margin.left,
695 grid_size.col_widths[col]
696 - grid_size.padding.right
697 - grid_size.padding.left
698 - grid_size.margin.right,
699 ];
700
701 charts.push(Chart {
702 axes: ChartAxes {
703 x: Some(AxisOptions {
704 position: Position::BOTTOM,
705 height,
706 padding: [grid_size.padding.left, grid_size.padding.right],
707 offset,
708 scale: x_opts.scale.clone(),
709 domain: x_opts.domain,
710 range,
711 clamp: x_opts.clamp,
712 font_size,
713 weight: 1.0,
714 tick_count: 3,
715 tick_labels: row == grid_size.num_rows - 1 || i == grid_size.num_items - 1,
716 major_ticks: Some(TickOptions {
717 font_size: font_size * 0.75,
718 weight: 1.0,
719 length: 8.0,
720 ..Default::default()
721 }),
722 minor_ticks: Some(TickOptions {
723 font_size: font_size * 0.75,
724 weight: 1.0,
725 length: 5.0,
726 ..Default::default()
727 }),
728 ..Default::default()
729 }),
730 y: Some(AxisOptions {
731 position: Position::LEFT,
732 height: width,
733 offset: grid_size.margin.left,
734 padding: [grid_size.padding.top, grid_size.padding.bottom],
735 scale: y_opts.scale.clone(),
736 domain: y_opts.domain,
737 range: y_range,
738 clamp: y_opts.clamp,
739 font_size,
740 weight: 1.0,
741 tick_count: 3,
742 tick_labels: col == 0,
743 major_ticks: Some(TickOptions {
744 font_size: font_size * 0.75,
745 weight: 1.0,
746 length: 8.0,
747 ..Default::default()
748 }),
749 minor_ticks: Some(TickOptions {
750 font_size: font_size * 0.75,
751 weight: 1.0,
752 length: 5.0,
753 ..Default::default()
754 }),
755 ..Default::default()
756 }),
757 ..Default::default()
758 },
759 scatter_data: Some(data.clone()),
760 dimensions: Dimensions {
761 height: grid_size.row_height,
762 width: grid_size.col_widths[col],
763 margin: grid_size.margin,
764 padding: grid_size.padding,
765 },
766 ..Default::default()
767 });
768 }
769
770 let processed_font_family = font_family("Roboto, Open sans, DejaVu Sans, Arial, sans-serif");
776
777 let mut document = Document::new()
778 .set("viewBox", (0, 0, grid_size.width, grid_size.height))
779 .add(
780 Rectangle::new()
781 .set("fill", "#ffffff")
782 .set("stroke", "none")
783 .set("width", grid_size.width)
784 .set("height", grid_size.height),
785 )
786 .add(
787 Text::new()
788 .set("font-family", processed_font_family.clone())
789 .set("font-size", font_size * 1.25)
790 .set("text-anchor", "middle")
791 .set("dominant-baseline", "middle")
792 .set("stroke", "none")
793 .set(
795 "transform",
796 format!(
797 "translate({:?}, {:?}) rotate({:?})",
798 grid_size.outer_margin.left / 2.0,
799 (grid_size.height
800 - grid_size.outer_margin.bottom
801 - grid_size.outer_margin.top)
802 / 2.0
803 + grid_size.outer_margin.top,
804 90
805 ),
806 )
807 .add(nodeText::new(y_label)),
808 )
809 .add(
810 Text::new()
811 .set("font-family", processed_font_family.clone())
812 .set("font-size", font_size * 1.25)
813 .set("text-anchor", "middle")
814 .set("dominant-baseline", "middle")
815 .set("stroke", "none")
816 .set(
818 "transform",
819 format!(
820 "translate({:?}, {:?})",
821 grid_size.outer_margin.left
822 + (grid_size.width
823 - grid_size.outer_margin.left
824 - grid_size.outer_margin.right)
825 / 2.0,
826 grid_size.height - grid_size.outer_margin.bottom / 2.0
827 ),
828 )
829 .add(nodeText::new(x_label)),
830 );
831 let mut i = 0;
832 let mut x_offset = grid_size.outer_margin.left;
833 for chart in charts {
834 let col = i / grid_size.num_rows;
835 let row = i % grid_size.num_rows;
836 if row == 0 && col > 0 {
837 x_offset += grid_size.col_widths[col - 1];
838 }
839 let y_offset = row as f64 * grid_size.row_height + grid_size.outer_margin.top;
840 let mut group =
841 Group::new().add(chart.svg(grid_size.margin.left, grid_size.margin.top, None));
842 if let Some(ref title) = titles[i] {
843 let mut title = title.clone();
844 if title.len() > 3 && title.len() as f64 * 10.0 > grid_size.col_widths[col] {
845 title = format!(
846 "{}...",
847 &title[..(grid_size.col_widths[col] / 10.0) as usize - 3]
848 );
849 }
850 group = group
851 .add(
852 Text::new()
853 .set("font-family", processed_font_family.clone())
854 .set("font-size", font_size * 0.75)
855 .set("text-anchor", "middle")
856 .set("dominant-baseline", "hanging")
857 .set("stroke", "none")
858 .set(
860 "transform",
861 format!(
862 "translate({:?}, {:?})",
863 grid_size.margin.left
864 + (grid_size.col_widths[col]
865 - grid_size.margin.left
866 - grid_size.margin.right)
867 / 2.0,
868 0.0
869 ),
870 )
871 .add(nodeText::new(title)),
872 )
873 .set(
874 "transform",
875 format!("translate({}, {})", x_offset, y_offset),
876 );
877 }
878 document = document.add(group);
879 i += 1;
880 }
881
882 document
883}
884
885pub fn legend(
886 blob_dimensions: BlobDimensions,
887 scatter_data: ScatterData,
888 options: &cli::PlotOptions,
889) -> Document {
890 let height = scatter_data.categories.len() * 26;
891
892 let mut width =
893 blob_dimensions.hist_width + blob_dimensions.margin.left + blob_dimensions.padding.left;
894
895 width = match options.show_legend {
896 ShowLegend::Compact => width,
897 _ => width + 220.0,
898 };
899
900 let offset_x = match options.show_legend {
901 ShowLegend::Compact => 0.0,
902 _ => width - 180.0,
903 };
904
905 let document = Document::new()
906 .set("viewBox", (0, 0, width, height))
907 .add(
908 Rectangle::new()
909 .set("fill", "#ffffff")
910 .set("stroke", "none")
911 .set("width", width)
912 .set("height", height),
913 )
914 .add(
915 category_legend_full(scatter_data.categories, options.show_legend.clone())
916 .set("transform", format!("translate({}, {})", offset_x, 10.0)),
917 );
918
919 document
920}