1use 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
68fn 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 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 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 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 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 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 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 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 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 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 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 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#[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}