charts_rs/charts/
component.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License at
4//
5//     http://www.apache.org/licenses/LICENSE-2.0
6//
7// Unless required by applicable law or agreed to in writing, software
8// distributed under the License is distributed on an "AS IS" BASIS,
9// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10// See the License for the specific language governing permissions and
11// limitations under the License.
12
13use serde::{Deserialize, Serialize};
14use snafu::{ResultExt, Snafu};
15use std::fmt;
16use std::vec;
17
18use super::color::*;
19use super::common::*;
20use super::font;
21use super::measure_text_width_family;
22use super::path::*;
23use super::util::*;
24
25static TAG_SVG: &str = "svg";
26static TAG_LINE: &str = "line";
27static TAG_RECT: &str = "rect";
28static TAG_POLYLINE: &str = "polyline";
29static TAG_CIRCLE: &str = "circle";
30static TAG_POLYGON: &str = "polygon";
31static TAG_TEXT: &str = "text";
32static TAG_PATH: &str = "path";
33static TAG_GROUP: &str = "g";
34
35static ATTR_VIEW_BOX: &str = "viewBox";
36static ATTR_XMLNS: &str = "xmlns";
37static ATTR_HEIGHT: &str = "height";
38static ATTR_WIDTH: &str = "width";
39static ATTR_FONT_FAMILY: &str = "font-family";
40static ATTR_FONT_SIZE: &str = "font-size";
41static ATTR_FONT_WEIGHT: &str = "font-weight";
42static ATTR_TRANSFORM: &str = "transform";
43static ATTR_DOMINANT_BASELINE: &str = "dominant-baseline";
44static ATTR_TEXT_ANCHOR: &str = "text-anchor";
45static ATTR_ALIGNMENT_BASELINE: &str = "alignment-baseline";
46static ATTR_STROKE_OPACITY: &str = "stroke-opacity";
47static ATTR_FILL_OPACITY: &str = "fill-opacity";
48static ATTR_STROKE_WIDTH: &str = "stroke-width";
49static ATTR_STROKE: &str = "stroke";
50static ATTR_STROKE_DASH_ARRAY: &str = "stroke-dasharray";
51static ATTR_X: &str = "x";
52static ATTR_Y: &str = "y";
53static ATTR_FILL: &str = "fill";
54static ATTR_X1: &str = "x1";
55static ATTR_Y1: &str = "y1";
56static ATTR_X2: &str = "x2";
57static ATTR_Y2: &str = "y2";
58static ATTR_RX: &str = "rx";
59static ATTR_RY: &str = "ry";
60static ATTR_POINTS: &str = "points";
61static ATTR_CX: &str = "cx";
62static ATTR_CY: &str = "cy";
63static ATTR_DX: &str = "dx";
64static ATTR_DY: &str = "dy";
65static ATTR_R: &str = "r";
66static ATTR_D: &str = "d";
67
68/// Converts opacity to string value.
69fn convert_opacity(color: &Color) -> String {
70    if color.is_nontransparent() {
71        "".to_string()
72    } else {
73        format_float(color.opacity())
74    }
75}
76
77fn format_option_float(value: Option<f32>) -> String {
78    if let Some(f) = value {
79        format_float(f)
80    } else {
81        "".to_string()
82    }
83}
84
85#[derive(Clone, PartialEq, Debug, Default)]
86struct SVGTag<'a> {
87    tag: &'a str,
88    attrs: Vec<(&'a str, String)>,
89    data: Option<String>,
90}
91
92pub fn generate_svg(width: f32, height: f32, x: f32, y: f32, data: String) -> String {
93    let mut attrs = vec![
94        (ATTR_WIDTH, format!("{}", width)),
95        (ATTR_HEIGHT, format!("{}", height)),
96        (ATTR_VIEW_BOX, format!("0 0 {} {}", width, height)),
97        (ATTR_XMLNS, "http://www.w3.org/2000/svg".to_string()),
98    ];
99    if x != 0.0 {
100        attrs.push((ATTR_X, format!("{}", x)))
101    }
102    if y != 0.0 {
103        attrs.push((ATTR_Y, format!("{}", y)))
104    }
105    SVGTag::new(TAG_SVG, data, attrs).to_string()
106}
107
108impl<'a> SVGTag<'a> {
109    pub fn new(tag: &'a str, data: String, attrs: Vec<(&'a str, String)>) -> Self {
110        Self {
111            tag,
112            attrs,
113            data: Some(data),
114        }
115    }
116}
117
118impl fmt::Display for SVGTag<'_> {
119    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
120        if self.tag == TAG_GROUP {
121            if let Some(ref data) = self.data {
122                if data.is_empty() {
123                    return write!(f, "");
124                }
125            }
126        }
127        let mut value = "<".to_string();
128        value.push_str(self.tag);
129        for (k, v) in self.attrs.iter() {
130            if k.is_empty() || v.is_empty() {
131                continue;
132            }
133            value.push(' ');
134            value.push_str(k);
135            value.push_str("=\"");
136            value.push_str(v);
137            value.push('\"');
138        }
139        if let Some(ref data) = self.data {
140            value.push_str(">\n");
141            value.push_str(data);
142            value.push_str(&format!("\n</{}>", self.tag));
143        } else {
144            value.push_str("/>");
145        }
146        write!(f, "{}", value)
147    }
148}
149
150#[derive(Debug, Snafu)]
151pub enum Error {
152    #[snafu(display("Error get font: {source}"))]
153    GetFont { source: font::Error },
154}
155
156pub type Result<T, E = Error> = std::result::Result<T, E>;
157
158pub enum Component {
159    Arrow(Arrow),
160    Bubble(Bubble),
161    Line(Line),
162    Rect(Rect),
163    Polyline(Polyline),
164    Circle(Circle),
165    Polygon(Polygon),
166    Text(Text),
167    SmoothLine(SmoothLine),
168    StraightLine(StraightLine),
169    SmoothLineFill(SmoothLineFill),
170    StraightLineFill(StraightLineFill),
171    Grid(Grid),
172    Axis(Axis),
173    Legend(Legend),
174    Pie(Pie),
175}
176#[derive(Clone, PartialEq, Debug)]
177
178pub struct Line {
179    pub color: Option<Color>,
180    pub stroke_width: f32,
181    pub left: f32,
182    pub top: f32,
183    pub right: f32,
184    pub bottom: f32,
185    // dash array
186    pub stroke_dash_array: Option<String>,
187}
188
189impl Default for Line {
190    fn default() -> Self {
191        Line {
192            color: None,
193            stroke_width: 1.0,
194            left: 0.0,
195            top: 0.0,
196            right: 0.0,
197            bottom: 0.0,
198            stroke_dash_array: None,
199        }
200    }
201}
202
203impl Line {
204    pub fn svg(&self) -> String {
205        if self.stroke_width <= 0.0 {
206            return "".to_string();
207        }
208        let mut attrs = vec![
209            (ATTR_STROKE_WIDTH, format_float(self.stroke_width)),
210            (ATTR_X1, format_float(self.left)),
211            (ATTR_Y1, format_float(self.top)),
212            (ATTR_X2, format_float(self.right)),
213            (ATTR_Y2, format_float(self.bottom)),
214        ];
215        if let Some(color) = self.color {
216            attrs.push((ATTR_STROKE, color.hex()));
217            attrs.push((ATTR_STROKE_OPACITY, convert_opacity(&color)));
218        }
219        if let Some(ref stroke_dash_array) = self.stroke_dash_array {
220            attrs.push((ATTR_STROKE_DASH_ARRAY, stroke_dash_array.to_string()));
221        }
222        SVGTag {
223            tag: TAG_LINE,
224            attrs,
225            data: None,
226        }
227        .to_string()
228    }
229}
230
231#[derive(Clone, PartialEq, Debug, Default)]
232pub struct Rect {
233    pub color: Option<Color>,
234    pub fill: Option<Color>,
235    pub left: f32,
236    pub top: f32,
237    pub width: f32,
238    pub height: f32,
239    pub rx: Option<f32>,
240    pub ry: Option<f32>,
241}
242impl Rect {
243    pub fn svg(&self) -> String {
244        let mut attrs = vec![
245            (ATTR_X, format_float(self.left)),
246            (ATTR_Y, format_float(self.top)),
247            (ATTR_WIDTH, format_float(self.width)),
248            (ATTR_HEIGHT, format_float(self.height)),
249            (ATTR_RX, format_option_float(self.rx)),
250            (ATTR_RY, format_option_float(self.ry)),
251        ];
252
253        if let Some(color) = self.color {
254            attrs.push((ATTR_STROKE, color.hex()));
255            attrs.push((ATTR_STROKE_OPACITY, convert_opacity(&color)));
256        }
257        if let Some(color) = self.fill {
258            if color.is_transparent() {
259                attrs.push((ATTR_FILL, "none".to_string()));
260            } else {
261                attrs.push((ATTR_FILL, color.hex()));
262                attrs.push((ATTR_FILL_OPACITY, convert_opacity(&color)));
263            }
264        }
265
266        SVGTag {
267            tag: TAG_RECT,
268            attrs,
269            data: None,
270        }
271        .to_string()
272    }
273}
274
275#[derive(Clone, PartialEq, Debug)]
276pub struct Polyline {
277    pub color: Option<Color>,
278    pub stroke_width: f32,
279    pub points: Vec<Point>,
280}
281
282impl Default for Polyline {
283    fn default() -> Self {
284        Polyline {
285            color: None,
286            stroke_width: 1.0,
287            points: vec![],
288        }
289    }
290}
291
292impl Polyline {
293    pub fn svg(&self) -> String {
294        if self.stroke_width <= 0.0 {
295            return "".to_string();
296        }
297        let points: Vec<String> = self
298            .points
299            .iter()
300            .map(|p| format!("{},{}", format_float(p.x), format_float(p.y)))
301            .collect();
302        let mut attrs = vec![
303            (ATTR_FILL, "none".to_string()),
304            (ATTR_STROKE_WIDTH, format_float(self.stroke_width)),
305            (ATTR_POINTS, points.join(" ")),
306        ];
307
308        if let Some(color) = self.color {
309            attrs.push((ATTR_STROKE, color.hex()));
310            attrs.push((ATTR_STROKE_OPACITY, convert_opacity(&color)));
311        }
312
313        SVGTag {
314            tag: TAG_POLYLINE,
315            attrs,
316            data: None,
317        }
318        .to_string()
319    }
320}
321
322#[derive(Clone, PartialEq, Debug)]
323pub struct Circle {
324    pub stroke_color: Option<Color>,
325    pub fill: Option<Color>,
326    pub stroke_width: f32,
327    pub cx: f32,
328    pub cy: f32,
329    pub r: f32,
330}
331
332impl Default for Circle {
333    fn default() -> Self {
334        Circle {
335            stroke_color: None,
336            fill: None,
337            stroke_width: 1.0,
338            cx: 0.0,
339            cy: 0.0,
340            r: 3.0,
341        }
342    }
343}
344
345impl Circle {
346    pub fn svg(&self) -> String {
347        let mut attrs = vec![
348            (ATTR_CX, format_float(self.cx)),
349            (ATTR_CY, format_float(self.cy)),
350            (ATTR_R, format_float(self.r)),
351            (ATTR_STROKE_WIDTH, format_float(self.stroke_width)),
352        ];
353        if let Some(color) = self.stroke_color {
354            attrs.push((ATTR_STROKE, color.hex()));
355            attrs.push((ATTR_STROKE_OPACITY, convert_opacity(&color)));
356        }
357        let mut fill = "none".to_string();
358        if let Some(color) = self.fill {
359            fill = color.hex();
360            attrs.push((ATTR_FILL_OPACITY, convert_opacity(&color)));
361        }
362        attrs.push((ATTR_FILL, fill));
363
364        SVGTag {
365            tag: TAG_CIRCLE,
366            attrs,
367            data: None,
368        }
369        .to_string()
370    }
371}
372
373#[derive(Clone, PartialEq, Debug)]
374pub struct Arrow {
375    pub x: f32,
376    pub y: f32,
377    pub width: f32,
378    pub stroke_color: Color,
379}
380impl Arrow {
381    pub fn default() -> Self {
382        Arrow {
383            x: 0.0,
384            y: 0.0,
385            width: 10.0,
386            stroke_color: Color::default(),
387        }
388    }
389    pub fn svg(&self) -> String {
390        let x_offset = self.width / 2.0;
391        let y_offset = self.width / 2.0;
392        let points = vec![
393            Point {
394                x: self.x,
395                y: self.y,
396            },
397            Point {
398                x: self.x - x_offset,
399                y: self.y - y_offset,
400            },
401            Point {
402                x: self.x + self.width,
403                y: self.y,
404            },
405            Point {
406                x: self.x - x_offset,
407                y: self.y + y_offset,
408            },
409        ];
410        StraightLine {
411            color: Some(self.stroke_color),
412            fill: Some(self.stroke_color),
413            points,
414            close: true,
415            symbol: None,
416            ..Default::default()
417        }
418        .svg()
419    }
420}
421
422#[derive(Clone, PartialEq, Debug, Default)]
423pub struct Polygon {
424    pub color: Option<Color>,
425    pub fill: Option<Color>,
426    pub points: Vec<Point>,
427}
428
429impl Polygon {
430    pub fn svg(&self) -> String {
431        if self.points.is_empty() {
432            return "".to_string();
433        }
434        let points: Vec<String> = self
435            .points
436            .iter()
437            .map(|p| format!("{},{}", format_float(p.x), format_float(p.y)))
438            .collect();
439        let mut attrs = vec![(ATTR_POINTS, points.join(" "))];
440        if let Some(color) = self.color {
441            attrs.push((ATTR_STROKE, color.hex()));
442            attrs.push((ATTR_STROKE_OPACITY, convert_opacity(&color)));
443        }
444        if let Some(color) = self.fill {
445            attrs.push((ATTR_FILL, color.hex()));
446            attrs.push((ATTR_FILL_OPACITY, convert_opacity(&color)));
447        }
448        SVGTag {
449            tag: TAG_POLYGON,
450            attrs,
451            data: None,
452        }
453        .to_string()
454    }
455}
456
457#[derive(Clone, PartialEq, Debug, Default)]
458pub struct Bubble {
459    pub r: f32,
460    pub x: f32,
461    pub y: f32,
462    pub fill: Color,
463}
464
465impl Bubble {
466    pub fn svg(&self) -> String {
467        let x = format_float(self.x);
468        let y = format_float(self.y);
469        let r = format_float(self.r);
470
471        let first = get_pie_point(self.x, self.y, self.r, -140.0);
472        let last = get_pie_point(self.x, self.y, self.r, 140.0);
473
474        let mut path_list = vec![
475            format!("M {},{}", format_float(first.x), format_float(first.y)),
476            format!("A {r},{r} 0,0,1 {},{y}", format_float(self.x - self.r)),
477            format!("A {r},{r} 0,0,1 {},{y}", format_float(self.x + self.r)),
478            format!(
479                "A {r},{r} 0,0,1 {},{}",
480                format_float(last.x),
481                format_float(last.y)
482            ),
483            format!("L {x},{}", format_float(self.y + self.r * 1.5)),
484        ];
485
486        path_list.push("Z".to_string());
487
488        let attrs = vec![
489            (ATTR_D, path_list.join(" ")),
490            (ATTR_FILL, self.fill.hex()),
491            (ATTR_FILL_OPACITY, convert_opacity(&self.fill)),
492        ];
493        SVGTag {
494            tag: TAG_PATH,
495            attrs,
496            ..Default::default()
497        }
498        .to_string()
499    }
500}
501
502#[derive(Clone, PartialEq, Debug, Default)]
503pub struct Text {
504    pub text: String,
505    pub font_family: Option<String>,
506    pub font_size: Option<f32>,
507    pub font_color: Option<Color>,
508    pub line_height: Option<f32>,
509    pub x: Option<f32>,
510    pub y: Option<f32>,
511    pub dx: Option<f32>,
512    pub dy: Option<f32>,
513    pub font_weight: Option<String>,
514    pub transform: Option<String>,
515    pub dominant_baseline: Option<String>,
516    pub text_anchor: Option<String>,
517    pub alignment_baseline: Option<String>,
518}
519
520impl Text {
521    pub fn svg(&self) -> String {
522        if self.text.is_empty() {
523            return "".to_string();
524        }
525        let mut attrs = vec![
526            (ATTR_FONT_SIZE, format_option_float(self.font_size)),
527            (ATTR_X, format_option_float(self.x)),
528            (ATTR_Y, format_option_float(self.y)),
529            (ATTR_DX, format_option_float(self.dx)),
530            (ATTR_DY, format_option_float(self.dy)),
531            (
532                ATTR_FONT_WEIGHT,
533                self.font_weight.clone().unwrap_or_default(),
534            ),
535            (ATTR_TRANSFORM, self.transform.clone().unwrap_or_default()),
536            (
537                ATTR_DOMINANT_BASELINE,
538                self.dominant_baseline.clone().unwrap_or_default(),
539            ),
540            (
541                ATTR_TEXT_ANCHOR,
542                self.text_anchor.clone().unwrap_or_default(),
543            ),
544            (
545                ATTR_ALIGNMENT_BASELINE,
546                self.alignment_baseline.clone().unwrap_or_default(),
547            ),
548        ];
549        if let Some(ref font_family) = self.font_family {
550            attrs.push((ATTR_FONT_FAMILY, font_family.clone()));
551        }
552        if let Some(color) = self.font_color {
553            attrs.push((ATTR_FILL, color.hex()));
554            attrs.push((ATTR_FILL_OPACITY, convert_opacity(&color)));
555        }
556
557        SVGTag {
558            tag: TAG_TEXT,
559            attrs,
560            data: Some(self.text.clone()),
561        }
562        .to_string()
563    }
564}
565
566fn generate_circle_symbol(points: &[Point], c: Circle) -> String {
567    let mut arr = vec![];
568    for p in points.iter() {
569        let mut tmp = c.clone();
570        tmp.cx = p.x;
571        tmp.cy = p.y;
572        arr.push(tmp.svg());
573    }
574    arr.join("\n")
575}
576
577#[derive(Clone, PartialEq, Debug)]
578pub struct Pie {
579    pub fill: Color,
580    pub stroke_color: Option<Color>,
581    pub cx: f32,
582    pub cy: f32,
583    pub r: f32,
584    pub ir: f32,
585    pub start_angle: f32,
586    pub delta: f32,
587    pub border_radius: f32,
588}
589
590impl Default for Pie {
591    fn default() -> Self {
592        Pie {
593            fill: (0, 0, 0).into(),
594            stroke_color: None,
595            cx: 0.0,
596            cy: 0.0,
597            r: 250.0,
598            ir: 60.0,
599            start_angle: 0.0,
600            delta: 0.0,
601            border_radius: 8.0,
602        }
603    }
604}
605
606impl Pie {
607    pub fn svg(&self) -> String {
608        let r = self.r;
609        let r_str = format_float(r);
610
611        let ir = self.ir;
612        let ir_str = format_float(ir);
613
614        let mut path_list = vec![];
615        let mut border_radius = self.border_radius;
616        if border_radius != 0.0 && self.r - self.ir < border_radius {
617            border_radius = 2.0;
618        }
619        let border_radius_str = format_float(border_radius);
620        let border_angle = 2.0_f32;
621        let start_angle = self.start_angle;
622        let end_angle = start_angle + self.delta;
623
624        // 左下角第一个点
625        if self.ir == 0.0 {
626            path_list.push(format!(
627                "M{},{}",
628                format_float(self.cx),
629                format_float(self.cy),
630            ));
631        } else {
632            let point = get_pie_point(self.cx, self.cy, self.ir + border_radius, start_angle);
633            path_list.push(format!(
634                "M{},{}",
635                format_float(point.x),
636                format_float(point.y)
637            ));
638        }
639
640        // 左侧直线
641        let point = get_pie_point(self.cx, self.cy, self.r - border_radius, start_angle);
642        path_list.push(format!(
643            "L{},{}",
644            format_float(point.x),
645            format_float(point.y)
646        ));
647
648        // 左上圆角
649        let point = get_pie_point(self.cx, self.cy, self.r, start_angle + border_angle);
650        path_list.push(format!(
651            "A{border_radius_str} {border_radius_str} 0 0 1 {},{}",
652            format_float(point.x),
653            format_float(point.y)
654        ));
655
656        // 大圆弧
657        // 如果过大,要先划一半
658        if self.delta > 180.0 {
659            let point = get_pie_point(
660                self.cx,
661                self.cy,
662                self.r,
663                self.start_angle + 180.0 - border_angle,
664            );
665            path_list.push(format!(
666                "A{r_str} {r_str} 0 0 1 {},{}",
667                format_float(point.x),
668                format_float(point.y)
669            ));
670        }
671
672        let point = get_pie_point(self.cx, self.cy, self.r, end_angle - border_angle);
673        path_list.push(format!(
674            "A{r_str} {r_str} 0 0 1 {},{}",
675            format_float(point.x),
676            format_float(point.y)
677        ));
678
679        // 右上圆角
680        let point = get_pie_point(self.cx, self.cy, self.r - border_radius, end_angle);
681        path_list.push(format!(
682            "A{border_radius_str} {border_radius_str} 0 0 1 {},{}",
683            format_float(point.x),
684            format_float(point.y)
685        ));
686
687        // 右侧直线
688        let point = get_pie_point(self.cx, self.cy, self.ir + border_radius, end_angle);
689        path_list.push(format!(
690            "L{},{}",
691            format_float(point.x),
692            format_float(point.y)
693        ));
694
695        if self.ir > 0.0 {
696            // 右下圆角
697            let point = get_pie_point(self.cx, self.cy, self.ir, end_angle - border_angle);
698            path_list.push(format!(
699                "A{border_radius_str} {border_radius_str} 0 0 1 {},{}",
700                format_float(point.x),
701                format_float(point.y)
702            ));
703
704            // 小圆弧
705            // 如果过大,要先划一半
706            if self.delta > 180.0 {
707                let point = get_pie_point(self.cx, self.cy, self.ir, end_angle - 180.0);
708                path_list.push(format!(
709                    "A{ir_str} {ir_str} 0 0 0 {},{}",
710                    format_float(point.x),
711                    format_float(point.y)
712                ));
713            }
714
715            let point = get_pie_point(self.cx, self.cy, self.ir, start_angle + border_angle);
716            path_list.push(format!(
717                "A{ir_str} {ir_str} 0 0 0 {},{}",
718                format_float(point.x),
719                format_float(point.y)
720            ));
721
722            // 左下圆角
723            let point = get_pie_point(self.cx, self.cy, self.ir + border_radius, start_angle);
724            path_list.push(format!(
725                "A{border_radius_str} {border_radius_str} 0 0 1 {},{}",
726                format_float(point.x),
727                format_float(point.y)
728            ));
729        }
730
731        path_list.push("Z".to_string());
732
733        let mut attrs = vec![
734            (ATTR_D, path_list.join(" ")),
735            (ATTR_FILL, self.fill.hex()),
736            (ATTR_FILL_OPACITY, convert_opacity(&self.fill)),
737        ];
738        if let Some(color) = self.stroke_color {
739            attrs.push((ATTR_STROKE, color.hex()));
740            attrs.push((ATTR_STROKE_OPACITY, convert_opacity(&color)));
741        }
742        SVGTag {
743            tag: TAG_PATH,
744            attrs,
745            ..Default::default()
746        }
747        .to_string()
748    }
749}
750
751struct BaseLine {
752    pub color: Option<Color>,
753    pub fill: Option<Color>,
754    pub points: Vec<Point>,
755    pub stroke_width: f32,
756    pub symbol: Option<Symbol>,
757    pub is_smooth: bool,
758    pub close: bool,
759    pub stroke_dash_array: Option<String>,
760}
761
762impl BaseLine {
763    pub fn svg(&self) -> String {
764        if self.points.is_empty() || self.stroke_width <= 0.0 {
765            return "".to_string();
766        }
767        let path = if self.is_smooth {
768            SmoothCurve {
769                points: self.points.clone(),
770                ..Default::default()
771            }
772            .to_string()
773        } else {
774            let mut arr = vec![];
775            for (index, p) in self.points.iter().enumerate() {
776                let mut action = "L";
777                if index == 0 {
778                    action = "M"
779                }
780                arr.push(format!(
781                    "{} {} {}",
782                    action,
783                    format_float(p.x),
784                    format_float(p.y)
785                ));
786            }
787            if self.close {
788                arr.push('Z'.to_string());
789            }
790            arr.join(" ")
791        };
792
793        let mut attrs = vec![
794            (ATTR_D, path),
795            (ATTR_STROKE_WIDTH, format_float(self.stroke_width)),
796        ];
797        if let Some(fill) = self.fill {
798            attrs.push((ATTR_FILL, fill.hex()));
799            attrs.push((ATTR_FILL_OPACITY, convert_opacity(&fill)));
800        } else {
801            attrs.push((ATTR_FILL, "none".to_string()));
802        }
803
804        if let Some(color) = self.color {
805            attrs.push((ATTR_STROKE, color.hex()));
806            attrs.push((ATTR_STROKE_OPACITY, convert_opacity(&color)));
807        }
808        if let Some(stroke_dash_array) = &self.stroke_dash_array {
809            attrs.push((ATTR_STROKE_DASH_ARRAY, stroke_dash_array.to_string()));
810        }
811        let line_svg = SVGTag {
812            tag: TAG_PATH,
813            attrs,
814            data: None,
815        }
816        .to_string();
817        let symbol_svg = if let Some(ref symbol) = self.symbol {
818            match symbol {
819                Symbol::Circle(r, fill) => generate_circle_symbol(
820                    &self.points,
821                    Circle {
822                        stroke_color: self.color,
823                        fill: fill.to_owned(),
824                        stroke_width: self.stroke_width,
825                        r: r.to_owned(),
826                        ..Default::default()
827                    },
828                ),
829                Symbol::None => "".to_string(),
830            }
831        } else {
832            "".to_string()
833        };
834
835        if symbol_svg.is_empty() {
836            line_svg
837        } else {
838            SVGTag {
839                tag: TAG_GROUP,
840                data: Some([line_svg, symbol_svg].join("\n")),
841                ..Default::default()
842            }
843            .to_string()
844        }
845    }
846}
847
848#[derive(Clone, PartialEq, Debug)]
849pub struct SmoothLine {
850    pub color: Option<Color>,
851    pub points: Vec<Point>,
852    pub stroke_width: f32,
853    pub symbol: Option<Symbol>,
854    pub stroke_dash_array: Option<String>,
855}
856
857impl Default for SmoothLine {
858    fn default() -> Self {
859        SmoothLine {
860            color: None,
861            points: vec![],
862            stroke_width: 1.0,
863            symbol: Some(Symbol::Circle(2.0, None)),
864            stroke_dash_array: None,
865        }
866    }
867}
868
869impl SmoothLine {
870    pub fn svg(&self) -> String {
871        BaseLine {
872            color: self.color,
873            fill: None,
874            points: self.points.clone(),
875            stroke_width: self.stroke_width,
876            symbol: self.symbol.clone(),
877            is_smooth: true,
878            close: false,
879            stroke_dash_array: self.stroke_dash_array.clone(),
880        }
881        .svg()
882    }
883}
884
885#[derive(Clone, PartialEq, Debug)]
886pub struct SmoothLineFill {
887    pub fill: Color,
888    pub points: Vec<Point>,
889    pub bottom: f32,
890}
891
892impl Default for SmoothLineFill {
893    fn default() -> Self {
894        SmoothLineFill {
895            fill: (255, 255, 255, 255).into(),
896            points: vec![],
897            bottom: 0.0,
898        }
899    }
900}
901
902impl SmoothLineFill {
903    pub fn svg(&self) -> String {
904        if self.points.is_empty() || self.fill.is_transparent() {
905            return "".to_string();
906        }
907        let mut path = SmoothCurve {
908            points: self.points.clone(),
909            ..Default::default()
910        }
911        .to_string();
912
913        let last = self.points[self.points.len() - 1];
914        let first = self.points[0];
915        let fill_path = [
916            format!("M {} {}", format_float(last.x), format_float(last.y)),
917            format!("L {} {}", format_float(last.x), format_float(self.bottom)),
918            format!("L {} {}", format_float(first.x), format_float(self.bottom)),
919            format!("L {} {}", format_float(first.x), format_float(first.y)),
920        ]
921        .join(" ");
922        path.push_str(&fill_path);
923
924        let attrs = vec![
925            (ATTR_D, path),
926            (ATTR_FILL, self.fill.hex()),
927            (ATTR_FILL_OPACITY, convert_opacity(&self.fill)),
928        ];
929
930        SVGTag {
931            tag: TAG_PATH,
932            attrs,
933            data: None,
934        }
935        .to_string()
936    }
937}
938
939#[derive(Clone, PartialEq, Debug)]
940pub struct StraightLine {
941    pub color: Option<Color>,
942    pub fill: Option<Color>,
943    pub points: Vec<Point>,
944    pub stroke_width: f32,
945    pub symbol: Option<Symbol>,
946    pub close: bool,
947    pub stroke_dash_array: Option<String>,
948}
949
950impl Default for StraightLine {
951    fn default() -> Self {
952        StraightLine {
953            color: None,
954            fill: None,
955            points: vec![],
956            stroke_width: 1.0,
957            symbol: Some(Symbol::Circle(2.0, None)),
958            close: false,
959            stroke_dash_array: None,
960        }
961    }
962}
963
964impl StraightLine {
965    pub fn svg(&self) -> String {
966        BaseLine {
967            color: self.color,
968            fill: self.fill,
969            points: self.points.clone(),
970            stroke_width: self.stroke_width,
971            symbol: self.symbol.clone(),
972            is_smooth: false,
973            close: self.close,
974            stroke_dash_array: self.stroke_dash_array.clone(),
975        }
976        .svg()
977    }
978}
979
980#[derive(Clone, PartialEq, Debug, Default)]
981pub struct StraightLineFill {
982    pub fill: Color,
983    pub points: Vec<Point>,
984    pub bottom: f32,
985    pub close: bool,
986}
987
988impl StraightLineFill {
989    pub fn svg(&self) -> String {
990        if self.points.is_empty() || self.fill.is_transparent() {
991            return "".to_string();
992        }
993        let mut points = self.points.clone();
994        let last = points[self.points.len() - 1];
995        let first = points[0];
996        points.push((last.x, self.bottom).into());
997        points.push((first.x, self.bottom).into());
998        points.push(first);
999        let mut arr = vec![];
1000        for (index, p) in points.iter().enumerate() {
1001            let mut action = "L";
1002            if index == 0 {
1003                action = "M"
1004            }
1005            arr.push(format!(
1006                "{} {} {}",
1007                action,
1008                format_float(p.x),
1009                format_float(p.y)
1010            ));
1011        }
1012        if self.close {
1013            arr.push('Z'.to_string());
1014        }
1015        let attrs = vec![
1016            (ATTR_D, arr.join(" ")),
1017            (ATTR_FILL, self.fill.hex()),
1018            (ATTR_FILL_OPACITY, convert_opacity(&self.fill)),
1019        ];
1020
1021        SVGTag {
1022            tag: TAG_PATH,
1023            attrs,
1024            data: None,
1025        }
1026        .to_string()
1027    }
1028}
1029
1030#[derive(Clone, PartialEq, Debug, Default)]
1031pub struct Grid {
1032    pub left: f32,
1033    pub top: f32,
1034    pub right: f32,
1035    pub bottom: f32,
1036    pub color: Option<Color>,
1037    pub stroke_width: f32,
1038    pub verticals: usize,
1039    pub hidden_verticals: Vec<usize>,
1040    pub horizontals: usize,
1041    pub hidden_horizontals: Vec<usize>,
1042}
1043
1044impl Grid {
1045    pub fn svg(&self) -> String {
1046        if (self.verticals == 0 && self.horizontals == 0) || self.stroke_width <= 0.0 {
1047            return "".to_string();
1048        }
1049        let mut points = vec![];
1050        if self.verticals != 0 {
1051            let unit = (self.right - self.left) / (self.verticals) as f32;
1052            for index in 0..=self.verticals {
1053                if self.hidden_verticals.contains(&index) {
1054                    continue;
1055                }
1056                let x = self.left + unit * index as f32;
1057                points.push((x, self.top, x, self.bottom));
1058            }
1059        }
1060        if self.horizontals != 0 {
1061            let unit = (self.bottom - self.top) / (self.horizontals) as f32;
1062            for index in 0..=self.horizontals {
1063                if self.hidden_horizontals.contains(&index) {
1064                    continue;
1065                }
1066                let y = self.top + unit * index as f32;
1067                points.push((self.left, y, self.right, y));
1068            }
1069        }
1070        let mut data = vec![];
1071        for (left, top, right, bottom) in points.iter() {
1072            let svg = Line {
1073                color: None,
1074                stroke_width: self.stroke_width,
1075                left: left.to_owned(),
1076                top: top.to_owned(),
1077                right: right.to_owned(),
1078                bottom: bottom.to_owned(),
1079                ..Default::default()
1080            }
1081            .svg();
1082            data.push(svg);
1083        }
1084
1085        let mut attrs = vec![];
1086        if let Some(color) = self.color {
1087            attrs.push((ATTR_STROKE, color.hex()));
1088            attrs.push((ATTR_STROKE_OPACITY, convert_opacity(&color)));
1089        }
1090
1091        SVGTag {
1092            tag: TAG_GROUP,
1093            attrs,
1094            data: Some(data.join("")),
1095        }
1096        .to_string()
1097    }
1098}
1099
1100#[derive(Clone, PartialEq, Debug)]
1101pub struct Axis {
1102    pub position: Position,
1103    pub split_number: usize,
1104    pub font_size: f32,
1105    pub font_family: String,
1106    pub font_color: Option<Color>,
1107    pub font_weight: Option<String>,
1108    pub data: Vec<String>,
1109    pub formatter: Option<String>,
1110    pub name_gap: f32,
1111    pub name_align: Align,
1112    pub name_rotate: f32,
1113    pub stroke_color: Option<Color>,
1114    pub left: f32,
1115    pub top: f32,
1116    pub width: f32,
1117    pub height: f32,
1118    pub tick_length: f32,
1119    pub tick_start: usize,
1120    pub tick_interval: usize,
1121}
1122impl Default for Axis {
1123    fn default() -> Self {
1124        Axis {
1125            position: Position::Bottom,
1126            split_number: 0,
1127            font_size: 14.0,
1128            font_family: font::DEFAULT_FONT_FAMILY.to_string(),
1129            data: vec![],
1130            formatter: None,
1131            font_color: None,
1132            font_weight: None,
1133            stroke_color: None,
1134            name_gap: 5.0,
1135            name_rotate: 0.0,
1136            name_align: Align::Center,
1137            left: 0.0,
1138            top: 0.0,
1139            width: 0.0,
1140            height: 0.0,
1141            tick_length: 5.0,
1142            tick_start: 0,
1143            tick_interval: 0,
1144        }
1145    }
1146}
1147
1148impl Axis {
1149    pub fn svg(&self) -> Result<String> {
1150        let left = self.left;
1151        let top = self.top;
1152        let width = self.width;
1153        let height = self.height;
1154        let tick_length = self.tick_length;
1155
1156        let mut attrs = vec![];
1157        let mut is_transparent = false;
1158        if let Some(color) = self.stroke_color {
1159            attrs.push((ATTR_STROKE, color.hex()));
1160            attrs.push((ATTR_STROKE_OPACITY, convert_opacity(&color)));
1161
1162            is_transparent = color.is_transparent();
1163        }
1164
1165        let stroke_width = 1.0;
1166
1167        let mut line_data = vec![];
1168        if !is_transparent {
1169            let values = match self.position {
1170                Position::Top => {
1171                    let y = top + height;
1172                    (left, y, left + width, y)
1173                }
1174                Position::Right => {
1175                    let y = top + height;
1176                    (left, top, left, y)
1177                }
1178                Position::Bottom => (left, top, left + width, top),
1179                _ => {
1180                    let x = left + width;
1181                    (x, top, x, top + height)
1182                }
1183            };
1184
1185            line_data.push(
1186                Line {
1187                    stroke_width,
1188                    left: values.0,
1189                    top: values.1,
1190                    right: values.2,
1191                    bottom: values.3,
1192                    ..Default::default()
1193                }
1194                .svg(),
1195            )
1196        }
1197
1198        let is_horizontal = self.position == Position::Bottom || self.position == Position::Top;
1199
1200        let axis_length = if is_horizontal {
1201            self.width
1202        } else {
1203            self.height
1204        };
1205        let font_size = self.font_size;
1206        let formatter = &self.formatter.clone().unwrap_or_default();
1207
1208        let mut text_list = vec![];
1209        let mut text_unit_count: usize = 1;
1210        if font_size > 0.0 && !self.data.is_empty() {
1211            text_list = self
1212                .data
1213                .iter()
1214                .map(|item| format_string(item, formatter))
1215                .collect();
1216            if self.position == Position::Top || self.position == Position::Bottom {
1217                let f = font::get_font(&self.font_family).context(GetFontSnafu)?;
1218                let total_measure = font::measure_text(f, font_size, &text_list.join(" "));
1219                let mut total_measure_width = total_measure.width();
1220                if self.name_rotate != 0.0 {
1221                    total_measure_width *= self.name_rotate.sin().abs();
1222                }
1223                // 位置不够
1224                if total_measure_width > axis_length {
1225                    text_unit_count += (total_measure_width / axis_length).ceil() as usize;
1226                }
1227            }
1228        }
1229
1230        let mut split_number = self.split_number;
1231        if split_number == 0 {
1232            split_number = self.data.len();
1233        }
1234        if !is_transparent {
1235            let unit = axis_length / split_number as f32;
1236            let tick_interval = self.tick_interval.max(text_unit_count);
1237            let tick_start = self.tick_start;
1238            for i in 0..=split_number {
1239                if i < tick_start {
1240                    continue;
1241                }
1242                let index = if i > tick_start { i - tick_start } else { i };
1243                if i != tick_start && (tick_interval != 0 && index % tick_interval != 0) {
1244                    continue;
1245                }
1246
1247                let values = match self.position {
1248                    Position::Top => {
1249                        let x = left + unit * i as f32;
1250                        let y = top + height;
1251                        (x, y - tick_length, x, y)
1252                    }
1253                    Position::Right => {
1254                        let y = top + unit * i as f32;
1255                        (left, y, left + tick_length, y)
1256                    }
1257                    Position::Bottom => {
1258                        let x = left + unit * i as f32;
1259                        (x, top, x, top + tick_length)
1260                    }
1261                    _ => {
1262                        let y = top + unit * i as f32;
1263                        let x = left + width;
1264                        (x, y, x - tick_length, y)
1265                    }
1266                };
1267
1268                line_data.push(
1269                    Line {
1270                        stroke_width,
1271                        left: values.0,
1272                        top: values.1,
1273                        right: values.2,
1274                        bottom: values.3,
1275                        ..Default::default()
1276                    }
1277                    .svg(),
1278                );
1279            }
1280        }
1281        let mut text_data = vec![];
1282        let name_rotate = self.name_rotate / std::f32::consts::PI * 180.0;
1283        if !text_list.is_empty() {
1284            let name_gap = self.name_gap;
1285            let f = font::get_font(&self.font_family).context(GetFontSnafu)?;
1286            let mut data_len = self.data.len();
1287            let is_name_align_start = self.name_align == Align::Left;
1288            if is_name_align_start {
1289                data_len -= 1;
1290            }
1291            let unit = axis_length / data_len as f32;
1292
1293            for (index, text) in text_list.iter().enumerate() {
1294                if index % text_unit_count != 0 {
1295                    continue;
1296                }
1297                let b = font::measure_text(f, font_size, text);
1298                let mut unit_offset = unit * index as f32 + unit / 2.0;
1299                if is_name_align_start {
1300                    unit_offset -= unit / 2.0;
1301                }
1302                let text_width = b.width();
1303
1304                let values = match self.position {
1305                    Position::Top => {
1306                        let y = top + height - name_gap;
1307                        let x = left + unit_offset - text_width / 2.0;
1308                        (x, y)
1309                    }
1310                    Position::Right => {
1311                        let x = left + name_gap;
1312                        let y = top + unit_offset + font_size / 2.0;
1313                        (x, y)
1314                    }
1315                    Position::Bottom => {
1316                        let y = top + font_size + name_gap;
1317                        let x = left + unit_offset - text_width / 2.0;
1318                        (x, y)
1319                    }
1320                    _ => {
1321                        let x = left + width - text_width - name_gap;
1322                        let y = top + unit_offset + font_size / 2.0 - 2.0;
1323                        (x, y)
1324                    }
1325                };
1326                let mut transform = None;
1327                let mut x = Some(values.0);
1328                let mut y = Some(values.1);
1329                let mut text_anchor = None;
1330                if name_rotate != 0.0 {
1331                    let w = self.name_rotate.sin().abs() * b.width();
1332                    let translate_x = (values.0 + b.width() / 2.0) as i32;
1333                    let translate_y = (values.1 + w / 2.0) as i32;
1334                    text_anchor = Some("middle".to_string());
1335
1336                    let a = name_rotate as i32;
1337                    transform = Some(format!(
1338                        "translate({translate_x},{translate_y}) rotate({a})"
1339                    ));
1340                    x = None;
1341                    y = None;
1342                }
1343
1344                text_data.push(
1345                    Text {
1346                        text: text.to_string(),
1347                        font_family: Some(self.font_family.clone()),
1348                        font_size: Some(self.font_size),
1349                        font_color: self.font_color,
1350                        font_weight: self.font_weight.clone(),
1351                        x,
1352                        y,
1353                        transform,
1354                        text_anchor,
1355                        ..Default::default()
1356                    }
1357                    .svg(),
1358                );
1359            }
1360        };
1361        Ok(SVGTag {
1362            tag: TAG_GROUP,
1363            data: Some(
1364                [
1365                    SVGTag {
1366                        tag: TAG_GROUP,
1367                        attrs,
1368                        data: Some(line_data.join("\n")),
1369                    }
1370                    .to_string(),
1371                    text_data.join("\n"),
1372                ]
1373                .join("\n"),
1374            ),
1375            ..Default::default()
1376        }
1377        .to_string())
1378    }
1379}
1380
1381pub(crate) static LEGEND_WIDTH: f32 = 25.0;
1382pub(crate) static LEGEND_HEIGHT: f32 = 20.0;
1383pub(crate) static LEGEND_TEXT_MARGIN: f32 = 3.0;
1384pub(crate) static LEGEND_MARGIN: f32 = 8.0;
1385
1386pub(crate) fn measure_legend_widths(
1387    font_family: &str,
1388    font_size: f32,
1389    legends: &[&str],
1390) -> Vec<f32> {
1391    legends
1392        .iter()
1393        .map(|item| {
1394            let text_box = measure_text_width_family(font_family, font_size, item.to_owned())
1395                .unwrap_or_default();
1396            text_box.width() + LEGEND_WIDTH + LEGEND_TEXT_MARGIN
1397        })
1398        .collect()
1399}
1400
1401pub(crate) fn wrap_legends_to_rows<'a>(
1402    font_family: &str,
1403    font_size: f32,
1404    legends: &[&'a str],
1405    max_width: f32,
1406) -> Vec<(f32, Vec<&'a str>)> {
1407    let widths = measure_legend_widths(font_family, font_size, legends);
1408    let mut rows = Vec::new();
1409    let mut current_row_legends = Vec::new();
1410    let mut current_row_width: f32 = 0.0;
1411
1412    for (&legend_str, &legend_width) in legends.iter().zip(widths.iter()) {
1413        let cost_to_add = if current_row_legends.is_empty() {
1414            legend_width
1415        } else {
1416            legend_width + LEGEND_MARGIN
1417        };
1418        if current_row_width + cost_to_add > max_width && !current_row_legends.is_empty() {
1419            rows.push((current_row_width, std::mem::take(&mut current_row_legends)));
1420
1421            current_row_legends.push(legend_str);
1422            current_row_width = legend_width;
1423        } else {
1424            current_row_legends.push(legend_str);
1425            current_row_width += cost_to_add;
1426        }
1427    }
1428
1429    if !current_row_legends.is_empty() {
1430        rows.push((current_row_width, current_row_legends));
1431    }
1432
1433    rows
1434}
1435
1436// pub(crate) fn measure_legends(
1437//     font_family: &str,
1438//     font_size: f32,
1439//     legends: &[&str],
1440// ) -> (f32, Vec<f32>) {
1441//     let widths = measure_legend_widths(font_family, font_size, legends);
1442//     let width: f32 = widths.iter().sum();
1443//     let margin = LEGEND_MARGIN * (legends.len() - 1) as f32;
1444
1445//     (width + margin, widths)
1446// }
1447
1448#[derive(Serialize, Deserialize, Clone, PartialEq, Debug, Default)]
1449pub enum LegendCategory {
1450    #[default]
1451    Normal,
1452    RoundRect,
1453    Circle,
1454    Rect,
1455}
1456
1457#[derive(Clone, PartialEq, Debug, Default)]
1458pub struct Legend {
1459    pub text: String,
1460    pub font_size: f32,
1461    pub font_family: String,
1462    pub font_color: Option<Color>,
1463    pub font_weight: Option<String>,
1464    pub stroke_color: Option<Color>,
1465    pub fill: Option<Color>,
1466    pub left: f32,
1467    pub top: f32,
1468    pub category: LegendCategory,
1469}
1470impl Legend {
1471    pub fn svg(&self) -> String {
1472        let stroke_width = 2.0;
1473        let mut data: Vec<String> = vec![];
1474        match self.category {
1475            LegendCategory::Rect => {
1476                let height = 10.0_f32;
1477                data.push(
1478                    Rect {
1479                        color: self.stroke_color,
1480                        fill: self.stroke_color,
1481                        left: self.left,
1482                        top: self.top + (LEGEND_HEIGHT - height) / 2.0,
1483                        width: LEGEND_WIDTH,
1484                        height,
1485                        ..Default::default()
1486                    }
1487                    .svg(),
1488                );
1489            }
1490            LegendCategory::RoundRect => {
1491                let height = 10.0_f32;
1492                data.push(
1493                    Rect {
1494                        color: self.stroke_color,
1495                        fill: self.stroke_color,
1496                        left: self.left,
1497                        top: self.top + (LEGEND_HEIGHT - height) / 2.0,
1498                        width: LEGEND_WIDTH,
1499                        height,
1500                        rx: Some(2.0),
1501                        ry: Some(2.0),
1502                    }
1503                    .svg(),
1504                );
1505            }
1506            LegendCategory::Circle => {
1507                data.push(
1508                    Circle {
1509                        stroke_width,
1510                        stroke_color: self.stroke_color,
1511                        fill: self.fill,
1512                        cx: self.left + LEGEND_WIDTH * 0.6,
1513                        cy: self.top + LEGEND_HEIGHT / 2.0,
1514                        r: 5.5,
1515                    }
1516                    .svg(),
1517                );
1518            }
1519            _ => {
1520                data.push(
1521                    Line {
1522                        stroke_width,
1523                        color: self.stroke_color,
1524                        left: self.left,
1525                        top: self.top + LEGEND_HEIGHT / 2.0,
1526                        right: self.left + LEGEND_WIDTH,
1527                        bottom: self.top + LEGEND_HEIGHT / 2.0,
1528                        ..Default::default()
1529                    }
1530                    .svg(),
1531                );
1532                data.push(
1533                    Circle {
1534                        stroke_width,
1535                        stroke_color: self.stroke_color,
1536                        fill: self.fill,
1537                        cx: self.left + LEGEND_WIDTH / 2.0,
1538                        cy: self.top + LEGEND_HEIGHT / 2.0,
1539                        r: 5.5,
1540                    }
1541                    .svg(),
1542                );
1543            }
1544        }
1545        data.push(
1546            Text {
1547                text: self.text.clone(),
1548                font_family: Some(self.font_family.clone()),
1549                font_color: self.font_color,
1550                font_size: Some(self.font_size),
1551                font_weight: self.font_weight.clone(),
1552                x: Some(self.left + LEGEND_WIDTH + LEGEND_TEXT_MARGIN),
1553                y: Some(self.top + self.font_size),
1554                ..Default::default()
1555            }
1556            .svg(),
1557        );
1558        SVGTag {
1559            tag: TAG_GROUP,
1560            data: Some(data.join("\n")),
1561            ..Default::default()
1562        }
1563        .to_string()
1564    }
1565}
1566
1567#[cfg(test)]
1568mod tests {
1569    use super::{
1570        wrap_legends_to_rows, Arrow, Axis, Bubble, Circle, Grid, Legend, LegendCategory, Line, Pie,
1571        Polygon, Polyline, Rect, SmoothLine, SmoothLineFill, StraightLine, StraightLineFill, Text,
1572    };
1573    use crate::{Align, Position, Symbol, DEFAULT_FONT_FAMILY};
1574    use pretty_assertions::assert_eq;
1575    #[test]
1576    fn test_line() {
1577        let line = Line::default();
1578        assert_eq!(1.0, line.stroke_width);
1579        assert_eq!(None, line.color);
1580
1581        assert_eq!(
1582            r###"<line stroke-width="1" x1="0" y1="1" x2="30" y2="5" stroke="#000000"/>"###,
1583            Line {
1584                color: Some((0, 0, 0).into()),
1585                stroke_width: 1.0,
1586                left: 0.0,
1587                top: 1.0,
1588                right: 30.0,
1589                bottom: 5.0,
1590                ..Default::default()
1591            }
1592            .svg()
1593        );
1594
1595        assert_eq!(
1596            r###"<line stroke-width="1" x1="0" y1="1" x2="30" y2="5" stroke="#000000" stroke-opacity="0.5"/>"###,
1597            Line {
1598                color: Some((0, 0, 0, 128).into()),
1599                stroke_width: 1.0,
1600                left: 0.0,
1601                top: 1.0,
1602                right: 30.0,
1603                bottom: 5.0,
1604                ..Default::default()
1605            }
1606            .svg()
1607        );
1608
1609        assert_eq!(
1610            r###"<line stroke-width="1" x1="0" y1="1" x2="30" y2="5"/>"###,
1611            Line {
1612                color: None,
1613                stroke_width: 1.0,
1614                left: 0.0,
1615                top: 1.0,
1616                right: 30.0,
1617                bottom: 5.0,
1618                ..Default::default()
1619            }
1620            .svg()
1621        );
1622
1623        assert_eq!(
1624            r###"<line stroke-width="1" x1="30" y1="10" x2="300" y2="10" stroke="#000000" stroke-opacity="0.5" stroke-dasharray="4,2"/>"###,
1625            Line {
1626                color: Some((0, 0, 0, 128).into()),
1627                stroke_width: 1.0,
1628                left: 30.0,
1629                top: 10.0,
1630                right: 300.0,
1631                bottom: 10.0,
1632                stroke_dash_array: Some("4,2".to_string()),
1633            }
1634            .svg()
1635        );
1636    }
1637
1638    #[test]
1639    fn test_rect() {
1640        assert_eq!(
1641            r###"<rect x="0" y="0" width="50" height="20" rx="3" ry="4" stroke="#000000" fill="#FFFFFF"/>"###,
1642            Rect {
1643                color: Some((0, 0, 0).into()),
1644                fill: Some((255, 255, 255).into()),
1645                left: 0.0,
1646                top: 0.0,
1647                width: 50.0,
1648                height: 20.0,
1649                rx: Some(3.0),
1650                ry: Some(4.0),
1651            }
1652            .svg()
1653        );
1654
1655        assert_eq!(
1656            r###"<rect x="0" y="0" width="50" height="20" rx="3" ry="4" stroke="#000000" stroke-opacity="0.5" fill="#FFFFFF" fill-opacity="0.2"/>"###,
1657            Rect {
1658                color: Some((0, 0, 0, 128).into()),
1659                fill: Some((255, 255, 255, 50).into()),
1660                left: 0.0,
1661                top: 0.0,
1662                width: 50.0,
1663                height: 20.0,
1664                rx: Some(3.0),
1665                ry: Some(4.0),
1666            }
1667            .svg()
1668        );
1669
1670        assert_eq!(
1671            r###"<rect x="0" y="0" width="50" height="20"/>"###,
1672            Rect {
1673                left: 0.0,
1674                top: 0.0,
1675                width: 50.0,
1676                height: 20.0,
1677                ..Default::default()
1678            }
1679            .svg()
1680        );
1681    }
1682
1683    #[test]
1684    fn test_bubble() {
1685        let c = Bubble {
1686            r: 15.0,
1687            x: 50.0,
1688            y: 50.0,
1689            fill: "#7EB26D".into(),
1690        };
1691
1692        assert_eq!(
1693            r###"<path d="M 40.4,61.5 A 15,15 0,0,1 35,50 A 15,15 0,0,1 65,50 A 15,15 0,0,1 59.6,61.5 L 50,72.5 Z" fill="#7EB26D"/>"###,
1694            c.svg()
1695        );
1696    }
1697
1698    #[test]
1699    fn test_polyline() {
1700        let polyline = Polyline::default();
1701        assert_eq!(1.0, polyline.stroke_width);
1702        assert_eq!(None, polyline.color);
1703
1704        assert_eq!(
1705            r###"<polyline fill="none" stroke-width="1" points="0,0 10,30 20,60 30,120" stroke="#000000"/>"###,
1706            Polyline {
1707                color: Some((0, 0, 0).into()),
1708                stroke_width: 1.0,
1709                points: vec![
1710                    (0.0, 0.0).into(),
1711                    (10.0, 30.0).into(),
1712                    (20.0, 60.0).into(),
1713                    (30.0, 120.0).into(),
1714                ]
1715            }
1716            .svg()
1717        );
1718
1719        assert_eq!(
1720            r###"<polyline fill="none" stroke-width="1" points="0,0 10,30 20,60 30,120" stroke="#000000" stroke-opacity="0.5"/>"###,
1721            Polyline {
1722                color: Some((0, 0, 0, 128).into()),
1723                stroke_width: 1.0,
1724                points: vec![
1725                    (0.0, 0.0).into(),
1726                    (10.0, 30.0).into(),
1727                    (20.0, 60.0).into(),
1728                    (30.0, 120.0).into(),
1729                ]
1730            }
1731            .svg()
1732        );
1733
1734        assert_eq!(
1735            r###"<polyline fill="none" stroke-width="1" points="0,0 10,30 20,60 30,120"/>"###,
1736            Polyline {
1737                color: None,
1738                stroke_width: 1.0,
1739                points: vec![
1740                    (0.0, 0.0).into(),
1741                    (10.0, 30.0).into(),
1742                    (20.0, 60.0).into(),
1743                    (30.0, 120.0).into(),
1744                ]
1745            }
1746            .svg()
1747        );
1748    }
1749
1750    #[test]
1751    fn test_circle() {
1752        let c = Circle::default();
1753        assert_eq!(None, c.stroke_color);
1754        assert_eq!(None, c.fill);
1755        assert_eq!(1.0, c.stroke_width);
1756        assert_eq!(3.0, c.r);
1757
1758        assert_eq!(
1759            r###"<circle cx="10" cy="10" r="3" stroke-width="1" stroke="#000000" fill="#FFFFFF"/>"###,
1760            Circle {
1761                stroke_color: Some((0, 0, 0).into()),
1762                fill: Some((255, 255, 255).into()),
1763                stroke_width: 1.0,
1764                cx: 10.0,
1765                cy: 10.0,
1766                r: 3.0,
1767            }
1768            .svg()
1769        );
1770
1771        assert_eq!(
1772            r###"<circle cx="10" cy="10" r="3" stroke-width="1" stroke="#000000" stroke-opacity="0.5" fill-opacity="0.1" fill="#FFFFFF"/>"###,
1773            Circle {
1774                stroke_color: Some((0, 0, 0, 128).into()),
1775                fill: Some((255, 255, 255, 20).into()),
1776                stroke_width: 1.0,
1777                cx: 10.0,
1778                cy: 10.0,
1779                r: 3.0,
1780            }
1781            .svg()
1782        );
1783
1784        assert_eq!(
1785            r###"<circle cx="10" cy="10" r="3" stroke-width="1" fill="none"/>"###,
1786            Circle {
1787                stroke_color: None,
1788                fill: None,
1789                stroke_width: 1.0,
1790                cx: 10.0,
1791                cy: 10.0,
1792                r: 3.0,
1793            }
1794            .svg()
1795        );
1796    }
1797
1798    #[test]
1799    fn test_arrow() {
1800        assert_eq!(
1801            r###"<path d="M 30 30 L 25 25 L 40 30 L 25 35 Z" stroke-width="1" fill="#7EB26D" stroke="#7EB26D"/>"###,
1802            Arrow {
1803                x: 30.0,
1804                y: 30.0,
1805                stroke_color: (126, 178, 109).into(),
1806                ..Arrow::default()
1807            }
1808            .svg()
1809        );
1810    }
1811
1812    #[test]
1813    fn test_polygon() {
1814        assert_eq!(
1815            r###"<polygon points="0,0 10,30 20,60 30,20" stroke="#000000" fill="#FFFFFF"/>"###,
1816            Polygon {
1817                color: Some((0, 0, 0).into()),
1818                fill: Some((255, 255, 255).into()),
1819                points: vec![
1820                    (0.0, 0.0).into(),
1821                    (10.0, 30.0).into(),
1822                    (20.0, 60.0).into(),
1823                    (30.0, 20.0).into(),
1824                ],
1825            }
1826            .svg()
1827        );
1828        assert_eq!(
1829            r###"<polygon points="0,0 10,30 20,60 30,20" stroke="#000000" stroke-opacity="0.5" fill="#FFFFFF" fill-opacity="0.1"/>"###,
1830            Polygon {
1831                color: Some((0, 0, 0, 128).into()),
1832                fill: Some((255, 255, 255, 20).into()),
1833                points: vec![
1834                    (0.0, 0.0).into(),
1835                    (10.0, 30.0).into(),
1836                    (20.0, 60.0).into(),
1837                    (30.0, 20.0).into(),
1838                ],
1839            }
1840            .svg()
1841        );
1842        assert_eq!(
1843            r###"<polygon points="0,0 10,30 20,60 30,20"/>"###,
1844            Polygon {
1845                color: None,
1846                fill: None,
1847                points: vec![
1848                    (0.0, 0.0).into(),
1849                    (10.0, 30.0).into(),
1850                    (20.0, 60.0).into(),
1851                    (30.0, 20.0).into(),
1852                ],
1853            }
1854            .svg()
1855        );
1856    }
1857
1858    #[test]
1859    fn test_text() {
1860        assert_eq!(
1861            r###"<text font-size="14" x="0" y="0" dx="5" dy="5" font-weight="bold" transform="translate(-36 45.5)" font-family="Roboto" fill="#000000">
1862Hello World!
1863</text>"###,
1864            Text {
1865                text: "Hello World!".to_string(),
1866                font_family: Some(DEFAULT_FONT_FAMILY.to_string()),
1867                font_size: Some(14.0),
1868                font_color: Some((0, 0, 0).into()),
1869                x: Some(0.0),
1870                y: Some(0.0),
1871                dy: Some(5.0),
1872                dx: Some(5.0),
1873                font_weight: Some("bold".to_string()),
1874                transform: Some("translate(-36 45.5)".to_string()),
1875                ..Default::default()
1876            }
1877            .svg()
1878        );
1879
1880        assert_eq!(
1881            r###"<text>
1882Hello World!
1883</text>"###,
1884            Text {
1885                text: "Hello World!".to_string(),
1886                ..Default::default()
1887            }
1888            .svg()
1889        );
1890    }
1891
1892    #[test]
1893    fn test_pie() {
1894        let p = Pie {
1895            fill: (0, 0, 0, 128).into(),
1896            stroke_color: Some((0, 0, 0).into()),
1897            cx: 250.0,
1898            cy: 250.0,
1899            r: 250.0,
1900            ir: 60.0,
1901            start_angle: 45.0,
1902            delta: 45.0,
1903            ..Default::default()
1904        };
1905        assert_eq!(
1906            r###"<path d="M298.1,201.9 L421.1,78.9 A8 8 0 0 1 432.8,79.5 A250 250 0 0 1 499.8,241.3 A8 8 0 0 1 492,250 L318,250 A8 8 0 0 1 310,247.9 A60 60 0 0 0 293.9,209.1 A8 8 0 0 1 298.1,201.9 Z" fill="#000000" fill-opacity="0.5" stroke="#000000"/>"###,
1907            p.svg()
1908        );
1909
1910        let p = Pie {
1911            fill: (0, 0, 0, 128).into(),
1912            stroke_color: Some((0, 0, 0).into()),
1913            cx: 250.0,
1914            cy: 250.0,
1915            r: 250.0,
1916            ir: 0.0,
1917            start_angle: 45.0,
1918            delta: 45.0,
1919            border_radius: 0.0,
1920        };
1921        assert_eq!(
1922            r###"<path d="M250,250 L426.8,73.2 A0 0 0 0 1 432.8,79.5 A250 250 0 0 1 499.8,241.3 A0 0 0 0 1 500,250 L250,250 Z" fill="#000000" fill-opacity="0.5" stroke="#000000"/>"###,
1923            p.svg()
1924        );
1925
1926        let p = Pie {
1927            fill: (0, 0, 0, 128).into(),
1928            stroke_color: Some((0, 0, 0).into()),
1929            cx: 250.0,
1930            cy: 250.0,
1931            r: 250.0,
1932            ir: 0.0,
1933            start_angle: 45.0,
1934            delta: 45.0,
1935            ..Default::default()
1936        };
1937        assert_eq!(
1938            r###"<path d="M250,250 L421.1,78.9 A8 8 0 0 1 432.8,79.5 A250 250 0 0 1 499.8,241.3 A8 8 0 0 1 492,250 L258,250 Z" fill="#000000" fill-opacity="0.5" stroke="#000000"/>"###,
1939            p.svg()
1940        );
1941
1942        let p = Pie {
1943            fill: (0, 0, 0, 128).into(),
1944            stroke_color: Some((0, 0, 0).into()),
1945            cx: 150.0,
1946            cy: 150.0,
1947            r: 50.0,
1948            ir: 25.0,
1949            start_angle: 45.0,
1950            delta: 230.0,
1951            ..Default::default()
1952        };
1953        assert_eq!(
1954            r###"<path d="M173.3,126.7 L179.7,120.3 A8 8 0 0 1 186.6,115.9 A50 50 0 0 1 115.9,186.6 A50 50 0 0 1 100.1,147.4 A8 8 0 0 1 108.2,146.3 L117.1,147.1 A8 8 0 0 1 125,148.7 A25 25 0 0 0 174.9,152.2 A25 25 0 0 0 168.3,133 A8 8 0 0 1 173.3,126.7 Z" fill="#000000" fill-opacity="0.5" stroke="#000000"/>"###,
1955            p.svg()
1956        );
1957    }
1958
1959    #[test]
1960    fn test_smooth_line() {
1961        let line = SmoothLine::default();
1962        assert_eq!(None, line.color);
1963        assert_eq!(1.0, line.stroke_width);
1964        assert_eq!(Some(Symbol::Circle(2.0, None)), line.symbol);
1965
1966        assert_eq!(
1967            r###"<g>
1968<path d="M0,0 C2.5 7.5, 8.1 22.3, 10 30 C13.1 42.3, 17.7 81.1, 20 80 C22.7 78.6, 26.7 24.9, 30 20 C31.7 17.4, 37.5 42.5, 40 50" stroke-width="1" fill="none" stroke="#000000"/>
1969<circle cx="0" cy="0" r="3" stroke-width="1" stroke="#000000" fill="#FFFFFF"/>
1970<circle cx="10" cy="30" r="3" stroke-width="1" stroke="#000000" fill="#FFFFFF"/>
1971<circle cx="20" cy="80" r="3" stroke-width="1" stroke="#000000" fill="#FFFFFF"/>
1972<circle cx="30" cy="20" r="3" stroke-width="1" stroke="#000000" fill="#FFFFFF"/>
1973<circle cx="40" cy="50" r="3" stroke-width="1" stroke="#000000" fill="#FFFFFF"/>
1974</g>"###,
1975            SmoothLine {
1976                color: Some((0, 0, 0).into()),
1977                points: vec![
1978                    (0.0, 0.0).into(),
1979                    (10.0, 30.0).into(),
1980                    (20.0, 80.0).into(),
1981                    (30.0, 20.0).into(),
1982                    (40.0, 50.0).into(),
1983                ],
1984                stroke_width: 1.0,
1985                symbol: Some(Symbol::Circle(3.0, Some((255, 255, 255).into()))),
1986                ..Default::default()
1987            }
1988            .svg()
1989        );
1990
1991        assert_eq!(
1992            r###"<path d="M0,0 C2.5 7.5, 8.1 22.3, 10 30 C13.1 42.3, 17.7 81.1, 20 80 C22.7 78.6, 26.7 24.9, 30 20 C31.7 17.4, 37.5 42.5, 40 50" stroke-width="1" fill="none"/>"###,
1993            SmoothLine {
1994                color: None,
1995                points: vec![
1996                    (0.0, 0.0).into(),
1997                    (10.0, 30.0).into(),
1998                    (20.0, 80.0).into(),
1999                    (30.0, 20.0).into(),
2000                    (40.0, 50.0).into(),
2001                ],
2002                stroke_width: 1.0,
2003                symbol: None,
2004                ..Default::default()
2005            }
2006            .svg()
2007        );
2008    }
2009
2010    #[test]
2011    fn test_straight_line() {
2012        let line = StraightLine::default();
2013        assert_eq!(None, line.color);
2014        assert_eq!(1.0, line.stroke_width);
2015        assert_eq!(Some(Symbol::Circle(2.0, None)), line.symbol);
2016
2017        assert_eq!(
2018            r###"<g>
2019<path d="M 0 0 L 10 30 L 20 80 L 30 20 L 40 50" stroke-width="1" fill="none" stroke="#000000"/>
2020<circle cx="0" cy="0" r="3" stroke-width="1" stroke="#000000" fill="none"/>
2021<circle cx="10" cy="30" r="3" stroke-width="1" stroke="#000000" fill="none"/>
2022<circle cx="20" cy="80" r="3" stroke-width="1" stroke="#000000" fill="none"/>
2023<circle cx="30" cy="20" r="3" stroke-width="1" stroke="#000000" fill="none"/>
2024<circle cx="40" cy="50" r="3" stroke-width="1" stroke="#000000" fill="none"/>
2025</g>"###,
2026            StraightLine {
2027                color: Some((0, 0, 0).into()),
2028                points: vec![
2029                    (0.0, 0.0).into(),
2030                    (10.0, 30.0).into(),
2031                    (20.0, 80.0).into(),
2032                    (30.0, 20.0).into(),
2033                    (40.0, 50.0).into(),
2034                ],
2035                stroke_width: 1.0,
2036                symbol: Some(Symbol::Circle(3.0, None)),
2037                ..Default::default()
2038            }
2039            .svg()
2040        );
2041
2042        assert_eq!(
2043            r###"<path d="M 0 0 L 10 30 L 20 80 L 30 20 L 40 50" stroke-width="1" fill="none"/>"###,
2044            StraightLine {
2045                color: None,
2046                points: vec![
2047                    (0.0, 0.0).into(),
2048                    (10.0, 30.0).into(),
2049                    (20.0, 80.0).into(),
2050                    (30.0, 20.0).into(),
2051                    (40.0, 50.0).into(),
2052                ],
2053                stroke_width: 1.0,
2054                symbol: None,
2055                ..Default::default()
2056            }
2057            .svg()
2058        );
2059    }
2060
2061    #[test]
2062    fn test_smooth_line_fill() {
2063        let fill = SmoothLineFill::default();
2064        assert_eq!(0.0, fill.bottom);
2065        assert_eq!("rgba(255,255,255,1.0)", fill.fill.rgba());
2066
2067        assert_eq!(
2068            r###"<path d="M0,0 C2.5 7.5, 8.1 22.3, 10 30 C13.1 42.3, 17.7 81.1, 20 80 C22.7 78.6, 26.7 24.9, 30 20 C31.7 17.4, 37.5 42.5, 40 50M 40 50 L 40 100 L 0 100 L 0 0" fill="#000000" fill-opacity="0.5"/>"###,
2069            SmoothLineFill {
2070                fill: (0, 0, 0, 128).into(),
2071                points: vec![
2072                    (0.0, 0.0).into(),
2073                    (10.0, 30.0).into(),
2074                    (20.0, 80.0).into(),
2075                    (30.0, 20.0).into(),
2076                    (40.0, 50.0).into(),
2077                ],
2078                bottom: 100.0,
2079            }
2080            .svg()
2081        );
2082    }
2083    #[test]
2084    fn test_straight_line_fill() {
2085        let fill = StraightLineFill::default();
2086        assert_eq!("rgba(0,0,0,0.0)", fill.fill.rgba());
2087        assert_eq!(0.0, fill.bottom);
2088
2089        assert_eq!(
2090            r###"<path d="M 0 0 L 10 30 L 20 80 L 30 20 L 40 50 L 40 100 L 0 100 L 0 0" fill="#000000" fill-opacity="0.5"/>"###,
2091            StraightLineFill {
2092                fill: (0, 0, 0, 128).into(),
2093                points: vec![
2094                    (0.0, 0.0).into(),
2095                    (10.0, 30.0).into(),
2096                    (20.0, 80.0).into(),
2097                    (30.0, 20.0).into(),
2098                    (40.0, 50.0).into(),
2099                ],
2100                bottom: 100.0,
2101                ..Default::default()
2102            }
2103            .svg()
2104        );
2105    }
2106
2107    #[test]
2108    fn test_grid() {
2109        assert_eq!(
2110            r###"<g stroke="#000000">
2111<line stroke-width="1" x1="58.3" y1="10" x2="58.3" y2="300"/><line stroke-width="1" x1="106.7" y1="10" x2="106.7" y2="300"/><line stroke-width="1" x1="155" y1="10" x2="155" y2="300"/><line stroke-width="1" x1="203.3" y1="10" x2="203.3" y2="300"/><line stroke-width="1" x1="251.7" y1="10" x2="251.7" y2="300"/><line stroke-width="1" x1="10" y1="68" x2="300" y2="68"/><line stroke-width="1" x1="10" y1="126" x2="300" y2="126"/><line stroke-width="1" x1="10" y1="184" x2="300" y2="184"/><line stroke-width="1" x1="10" y1="242" x2="300" y2="242"/>
2112</g>"###,
2113            Grid {
2114                left: 10.0,
2115                top: 10.0,
2116                right: 300.0,
2117                bottom: 300.0,
2118                color: Some((0, 0, 0).into()),
2119                stroke_width: 1.0,
2120                verticals: 6,
2121                hidden_verticals: vec![0, 6],
2122                horizontals: 5,
2123                hidden_horizontals: vec![0, 5],
2124            }
2125            .svg()
2126        );
2127    }
2128    #[test]
2129    fn test_axis() {
2130        let a = Axis::default();
2131        assert_eq!(Position::Bottom, a.position);
2132        assert_eq!(14.0, a.font_size);
2133        assert_eq!(DEFAULT_FONT_FAMILY, a.font_family);
2134        assert_eq!(None, a.font_color);
2135        assert_eq!(None, a.stroke_color);
2136        assert_eq!(5.0, a.name_gap);
2137        assert_eq!(Align::Center, a.name_align);
2138        assert_eq!(5.0, a.tick_length);
2139
2140        assert_eq!(
2141            r###"<g>
2142<g stroke="#000000">
2143<line stroke-width="1" x1="0" y1="50" x2="300" y2="50"/>
2144<line stroke-width="1" x1="0" y1="50" x2="0" y2="55"/>
2145<line stroke-width="1" x1="42.9" y1="50" x2="42.9" y2="55"/>
2146<line stroke-width="1" x1="85.7" y1="50" x2="85.7" y2="55"/>
2147<line stroke-width="1" x1="128.6" y1="50" x2="128.6" y2="55"/>
2148<line stroke-width="1" x1="171.4" y1="50" x2="171.4" y2="55"/>
2149<line stroke-width="1" x1="214.3" y1="50" x2="214.3" y2="55"/>
2150<line stroke-width="1" x1="257.1" y1="50" x2="257.1" y2="55"/>
2151<line stroke-width="1" x1="300" y1="50" x2="300" y2="55"/>
2152</g>
2153<text font-size="14" x="7.4" y="69" font-family="Roboto" fill="#000000">
2154Mon
2155</text>
2156<text font-size="14" x="52.3" y="69" font-family="Roboto" fill="#000000">
2157Tue
2158</text>
2159<text font-size="14" x="93.1" y="69" font-family="Roboto" fill="#000000">
2160Wed
2161</text>
2162<text font-size="14" x="138" y="69" font-family="Roboto" fill="#000000">
2163Thu
2164</text>
2165<text font-size="14" x="184.9" y="69" font-family="Roboto" fill="#000000">
2166Fri
2167</text>
2168<text font-size="14" x="224.7" y="69" font-family="Roboto" fill="#000000">
2169Sat
2170</text>
2171<text font-size="14" x="266.6" y="69" font-family="Roboto" fill="#000000">
2172Sun
2173</text>
2174</g>"###,
2175            Axis {
2176                position: Position::Bottom,
2177                split_number: 7,
2178                font_color: Some((0, 0, 0).into()),
2179                data: vec![
2180                    "Mon".to_string(),
2181                    "Tue".to_string(),
2182                    "Wed".to_string(),
2183                    "Thu".to_string(),
2184                    "Fri".to_string(),
2185                    "Sat".to_string(),
2186                    "Sun".to_string(),
2187                ],
2188                stroke_color: Some((0, 0, 0).into()),
2189                left: 0.0,
2190                top: 50.0,
2191                width: 300.0,
2192                height: 30.0,
2193                ..Default::default()
2194            }
2195            .svg()
2196            .unwrap()
2197        );
2198    }
2199
2200    #[test]
2201    fn test_legend() {
2202        assert_eq!(
2203            r###"<g>
2204<line stroke-width="2" x1="10" y1="40" x2="35" y2="40" stroke="#000000"/>
2205<circle cx="22.5" cy="40" r="5.5" stroke-width="2" stroke="#000000" fill="#000000"/>
2206<text font-size="14" x="38" y="44" font-family="Roboto" fill="#000000">
2207Line
2208</text>
2209</g>"###,
2210            Legend {
2211                text: "Line".to_string(),
2212                font_size: 14.0,
2213                font_family: DEFAULT_FONT_FAMILY.to_string(),
2214                font_color: Some((0, 0, 0).into()),
2215                stroke_color: Some((0, 0, 0).into()),
2216                fill: Some((0, 0, 0).into()),
2217                left: 10.0,
2218                top: 30.0,
2219                ..Default::default()
2220            }
2221            .svg()
2222        );
2223
2224        assert_eq!(
2225            r###"<g>
2226<rect x="10" y="35" width="25" height="10" stroke="#000000" fill="#000000"/>
2227<text font-size="14" x="38" y="44" font-family="Roboto" fill="#000000">
2228Line
2229</text>
2230</g>"###,
2231            Legend {
2232                text: "Line".to_string(),
2233                font_size: 14.0,
2234                font_family: DEFAULT_FONT_FAMILY.to_string(),
2235                font_color: Some((0, 0, 0).into()),
2236                stroke_color: Some((0, 0, 0).into()),
2237                fill: Some((0, 0, 0).into()),
2238                left: 10.0,
2239                top: 30.0,
2240                category: LegendCategory::Rect,
2241                ..Default::default()
2242            }
2243            .svg()
2244        );
2245    }
2246
2247    #[test]
2248    fn test_wrap_legends_to_rows() {
2249        let rows = wrap_legends_to_rows(
2250            DEFAULT_FONT_FAMILY,
2251            14.0,
2252            &[
2253                "M1 - End of Inception",
2254                "M2 - End of Elaboration",
2255                "M3 - End of Construction",
2256                "M4 - End of Completion",
2257            ],
2258            600.0,
2259        );
2260        assert_eq!(
2261            rows,
2262            vec![
2263                (
2264                    551.0,
2265                    vec![
2266                        "M1 - End of Inception",
2267                        "M2 - End of Elaboration",
2268                        "M3 - End of Construction"
2269                    ]
2270                ),
2271                (181.0, vec!["M4 - End of Completion"])
2272            ]
2273        );
2274    }
2275}