Skip to main content

blobtk/plot/
blob.rs

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    // if domain[0] == 0.005 {
263    //     domain = [0.0, 1.0];
264    // }
265    (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    // if y_domain[0] == y_domain[1] {
301    //     if y_domain[0] == 0.0 {
302    //         y_domain[1] += 0.1;
303    //     } else {
304    //         y_domain[0] /= 2.0;
305    //         y_domain[1] *= 2.0;
306    //     }
307    // }
308    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            // TODO: add option to keep points together
379            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; // - grid_size.margin.bottom;
671                                                                 // let y_range = [
672                                                                 //     grid_size.row_height - grid_size.margin.top - grid_size.margin.bottom,
673                                                                 //     grid_size.margin.bottom,
674                                                                 // ];
675    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    // let line_weight = 2.0;
685
686    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 legend_x = match options.show_legend {
771    //     ShowLegend::Compact => grid_size.width - 100.0,
772    //     _ => grid_size.width - 185.0,
773    // };
774
775    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("fill", options.font_color.clone())
794                .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("fill", options.font_color.clone())
817                .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("fill", options.font_color.clone())
859                        .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}