diff --git a/.vscode/settings.json b/.vscode/settings.json
index d53e8ef..69d0337 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -41,6 +41,7 @@
"edgecolors",
"evcxr",
"facecolor",
+ "facecolors",
"fileio",
"fontsize",
"fontweight",
@@ -76,6 +77,7 @@
"polyline",
"pyplot",
"rarrow",
+ "rgba",
"roundtooth",
"rstride",
"savefig",
@@ -87,6 +89,8 @@
"TICKRIGHT",
"TICKUP",
"toolkits",
+ "triplot",
+ "trisurf",
"twinx",
"verticalalignment",
"whis",
diff --git a/Cargo.toml b/Cargo.toml
index 7627490..0757bf4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "plotpy"
-version = "1.13.0"
+version = "1.13.1"
edition = "2021"
license = "MIT"
description = "Rust plotting library using Python (Matplotlib)"
diff --git a/examples/README.md b/examples/README.md
index 03eea59..a11494b 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -95,6 +95,8 @@ Some output of integration tests are shown below.
## Image
[test_image.rs](https://github.com/cpmech/plotpy/tree/main/tests/test_image.rs)
+[test_image_with_rgb.rs](https://github.com/cpmech/plotpy/tree/main/tests/test_image_with_rgb.rs)
+[test_image_with_rgba.rs](https://github.com/cpmech/plotpy/tree/main/tests/test_image_with_rgba.rs)

diff --git a/figures/doc_fill_between.svg b/figures/doc_fill_between.svg
new file mode 100644
index 0000000..f4a7b17
--- /dev/null
+++ b/figures/doc_fill_between.svg
@@ -0,0 +1,538 @@
+
+
+
diff --git a/figures/integ_canvas_draw_triangles.svg b/figures/integ_canvas_draw_triangles.svg
new file mode 100644
index 0000000..a7c93ce
--- /dev/null
+++ b/figures/integ_canvas_draw_triangles.svg
@@ -0,0 +1,441 @@
+
+
+
diff --git a/figures/integ_canvas_draw_triangles_3d.svg b/figures/integ_canvas_draw_triangles_3d.svg
new file mode 100644
index 0000000..50351f8
--- /dev/null
+++ b/figures/integ_canvas_draw_triangles_3d.svg
@@ -0,0 +1,1149 @@
+
+
+
diff --git a/figures/integ_fill_between_1.svg b/figures/integ_fill_between_1.svg
new file mode 100644
index 0000000..ac54cbf
--- /dev/null
+++ b/figures/integ_fill_between_1.svg
@@ -0,0 +1,474 @@
+
+
+
diff --git a/figures/integ_fill_between_2.svg b/figures/integ_fill_between_2.svg
new file mode 100644
index 0000000..b64ecb4
--- /dev/null
+++ b/figures/integ_fill_between_2.svg
@@ -0,0 +1,484 @@
+
+
+
diff --git a/figures/integ_fill_between_3.svg b/figures/integ_fill_between_3.svg
new file mode 100644
index 0000000..1fcfc85
--- /dev/null
+++ b/figures/integ_fill_between_3.svg
@@ -0,0 +1,538 @@
+
+
+
diff --git a/figures/integ_image_with_rgb.svg b/figures/integ_image_with_rgb.svg
new file mode 100644
index 0000000..804cd07
--- /dev/null
+++ b/figures/integ_image_with_rgb.svg
@@ -0,0 +1,401 @@
+
+
+
diff --git a/figures/integ_image_with_rgba.svg b/figures/integ_image_with_rgba.svg
new file mode 100644
index 0000000..21874b4
--- /dev/null
+++ b/figures/integ_image_with_rgba.svg
@@ -0,0 +1,401 @@
+
+
+
diff --git a/figures/integ_surface.svg b/figures/integ_surface.svg
index 4c61240..cbe564c 100644
--- a/figures/integ_surface.svg
+++ b/figures/integ_surface.svg
@@ -6,7 +6,7 @@
- 2024-09-16T08:13:04.740749
+ 2025-11-18T08:04:45.360954
image/svg+xml
@@ -42,7 +42,7 @@ z
L 115.172164 134.035495
L 113.950564 27.801991
L 21.866234 95.000941
-" style="fill: #f2f2f2; opacity: 0.5; stroke: #f2f2f2; stroke-linejoin: miter"/>
+" style="fill: #ffffff; opacity: 0.5; stroke: #ffffff; stroke-linejoin: miter"/>
@@ -51,7 +51,7 @@ L 21.866234 95.000941
L 256.186401 175.022766
L 261.218702 65.130171
L 113.950564 27.801991
-" style="fill: #e6e6e6; opacity: 0.5; stroke: #e6e6e6; stroke-linejoin: miter"/>
+" style="fill: #ffffff; opacity: 0.5; stroke: #ffffff; stroke-linejoin: miter"/>
@@ -60,7 +60,7 @@ L 113.950564 27.801991
L 176.775269 256.518123
L 256.186401 175.022766
L 115.172164 134.035495
-" style="fill: #ececec; opacity: 0.5; stroke: #ececec; stroke-linejoin: miter"/>
+" style="fill: #ffffff; opacity: 0.5; stroke: #ffffff; stroke-linejoin: miter"/>
@@ -92,28 +92,6 @@ z
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+" clip-path="url(#p8e4a585976)" style="fill: #e5d8bd"/>
+" clip-path="url(#p8e4a585976)" style="fill: #ffffcc"/>
+" clip-path="url(#p8e4a585976)" style="fill: #fed9a6"/>
+" clip-path="url(#p8e4a585976)" style="fill: #decbe4"/>
+" clip-path="url(#p8e4a585976)" style="fill: #f2f2f2"/>
+" clip-path="url(#p8e4a585976)" style="fill: #fed9a6"/>
+" clip-path="url(#p8e4a585976)" style="fill: #e5d8bd"/>
+" clip-path="url(#p8e4a585976)" style="fill: #ccebc5"/>
+" clip-path="url(#p8e4a585976)" style="fill: #decbe4"/>
+" clip-path="url(#p8e4a585976)" style="fill: #b3cde3"/>
+" clip-path="url(#p8e4a585976)" style="fill: #ffffcc"/>
+" clip-path="url(#p8e4a585976)" style="fill: #fed9a6"/>
+" clip-path="url(#p8e4a585976)" style="fill: #fbb4ae"/>
+" clip-path="url(#p8e4a585976)" style="fill: #ccebc5"/>
+" clip-path="url(#p8e4a585976)" style="fill: #ccebc5"/>
+" clip-path="url(#p8e4a585976)" style="fill: #fbb4ae"/>
+" clip-path="url(#p8e4a585976)" style="fill: #fbb4ae"/>
+" clip-path="url(#p8e4a585976)" style="fill: #fed9a6"/>
+" clip-path="url(#p8e4a585976)" style="fill: #fbb4ae"/>
+" clip-path="url(#p8e4a585976)" style="fill: #ffffcc"/>
+" clip-path="url(#p8e4a585976)" style="fill: #decbe4"/>
+" clip-path="url(#p8e4a585976)" style="fill: #fbb4ae"/>
+" clip-path="url(#p8e4a585976)" style="fill: #ccebc5"/>
+" clip-path="url(#p8e4a585976)" style="fill: #fbb4ae"/>
+" clip-path="url(#p8e4a585976)" style="fill: #b3cde3"/>
+" clip-path="url(#p8e4a585976)" style="fill: #fed9a6"/>
+" clip-path="url(#p8e4a585976)" style="fill: #fbb4ae"/>
+" clip-path="url(#p8e4a585976)" style="fill: #e5d8bd"/>
+" clip-path="url(#p8e4a585976)" style="fill: #fed9a6"/>
+" clip-path="url(#p8e4a585976)" style="fill: #fbb4ae"/>
+" clip-path="url(#p8e4a585976)" style="fill: #fbb4ae"/>
+" clip-path="url(#p8e4a585976)" style="fill: #decbe4"/>
+" clip-path="url(#p8e4a585976)" style="fill: #decbe4"/>
+" clip-path="url(#p8e4a585976)" style="fill: #b3cde3"/>
+" clip-path="url(#p8e4a585976)" style="fill: #ffffcc"/>
+" clip-path="url(#p8e4a585976)" style="fill: #fbb4ae"/>
+" clip-path="url(#p8e4a585976)" style="fill: #fbb4ae"/>
+" clip-path="url(#p8e4a585976)" style="fill: #ccebc5"/>
+" clip-path="url(#p8e4a585976)" style="fill: #f2f2f2"/>
+" clip-path="url(#p8e4a585976)" style="fill: #e5d8bd"/>
+" clip-path="url(#p8e4a585976)" style="fill: #fbb4ae"/>
+" clip-path="url(#p8e4a585976)" style="fill: #ffffcc"/>
+" clip-path="url(#p8e4a585976)" style="fill: #fed9a6"/>
+" clip-path="url(#p8e4a585976)" style="fill: #decbe4"/>
+" clip-path="url(#p8e4a585976)" style="fill: #ccebc5"/>
+" clip-path="url(#p8e4a585976)" style="fill: #b3cde3"/>
+" clip-path="url(#p8e4a585976)" style="fill: #e5d8bd"/>
+" clip-path="url(#p8e4a585976)" style="fill: #ccebc5"/>
+" clip-path="url(#p8e4a585976)" style="fill: #fed9a6"/>
+" clip-path="url(#p8e4a585976)" style="fill: #ccebc5"/>
+" clip-path="url(#p8e4a585976)" style="fill: #decbe4"/>
+" clip-path="url(#p8e4a585976)" style="fill: #fed9a6"/>
+" clip-path="url(#p8e4a585976)" style="fill: #decbe4"/>
+" clip-path="url(#p8e4a585976)" style="fill: #e5d8bd"/>
+" clip-path="url(#p8e4a585976)" style="fill: #ffffcc"/>
+" clip-path="url(#p8e4a585976)" style="fill: #f2f2f2"/>
+" clip-path="url(#p8e4a585976)" style="fill: #fed9a6"/>
+" clip-path="url(#p8e4a585976)" style="fill: #fed9a6"/>
+" clip-path="url(#p8e4a585976)" style="fill: #ffffcc"/>
+" clip-path="url(#p8e4a585976)" style="fill: #fed9a6"/>
+" clip-path="url(#p8e4a585976)" style="fill: #ffffcc"/>
+" clip-path="url(#p8e4a585976)" style="fill: #e5d8bd"/>
+" clip-path="url(#p8e4a585976)" style="fill: #e5d8bd"/>
+" clip-path="url(#p8e4a585976)" style="fill: #f2f2f2"/>
-
+
+" clip-path="url(#p8e4a585976)" style="fill: none; stroke-dasharray: 0.75,1.2375; stroke-dashoffset: 0; stroke: #1862ab; stroke-width: 0.75"/>
+" clip-path="url(#p8e4a585976)" style="fill: none; stroke-dasharray: 0.75,1.2375; stroke-dashoffset: 0; stroke: #1862ab; stroke-width: 0.75"/>
+" clip-path="url(#p8e4a585976)" style="fill: none; stroke-dasharray: 0.75,1.2375; stroke-dashoffset: 0; stroke: #1862ab; stroke-width: 0.75"/>
+" clip-path="url(#p8e4a585976)" style="fill: none; stroke-dasharray: 0.75,1.2375; stroke-dashoffset: 0; stroke: #1862ab; stroke-width: 0.75"/>
+" clip-path="url(#p8e4a585976)" style="fill: none; stroke-dasharray: 0.75,1.2375; stroke-dashoffset: 0; stroke: #1862ab; stroke-width: 0.75"/>
+" clip-path="url(#p8e4a585976)" style="fill: none; stroke-dasharray: 0.75,1.2375; stroke-dashoffset: 0; stroke: #1862ab; stroke-width: 0.75"/>
+" clip-path="url(#p8e4a585976)" style="fill: none; stroke-dasharray: 0.75,1.2375; stroke-dashoffset: 0; stroke: #1862ab; stroke-width: 0.75"/>
+" clip-path="url(#p8e4a585976)" style="fill: none; stroke-dasharray: 0.75,1.2375; stroke-dashoffset: 0; stroke: #1862ab; stroke-width: 0.75"/>
+" clip-path="url(#p8e4a585976)" style="fill: none; stroke-dasharray: 0.75,1.2375; stroke-dashoffset: 0; stroke: #1862ab; stroke-width: 0.75"/>
+" clip-path="url(#p8e4a585976)" style="fill: none; stroke-dasharray: 0.75,1.2375; stroke-dashoffset: 0; stroke: #1862ab; stroke-width: 0.75"/>
+" clip-path="url(#p8e4a585976)" style="fill: none; stroke-dasharray: 0.75,1.2375; stroke-dashoffset: 0; stroke: #1862ab; stroke-width: 0.75"/>
+" clip-path="url(#p8e4a585976)" style="fill: none; stroke-dasharray: 0.75,1.2375; stroke-dashoffset: 0; stroke: #1862ab; stroke-width: 0.75"/>
+" clip-path="url(#p8e4a585976)" style="fill: none; stroke-dasharray: 0.75,1.2375; stroke-dashoffset: 0; stroke: #1862ab; stroke-width: 0.75"/>
+" clip-path="url(#p8e4a585976)" style="fill: none; stroke-dasharray: 0.75,1.2375; stroke-dashoffset: 0; stroke: #1862ab; stroke-width: 0.75"/>
+" clip-path="url(#p8e4a585976)" style="fill: none; stroke-dasharray: 0.75,1.2375; stroke-dashoffset: 0; stroke: #1862ab; stroke-width: 0.75"/>
+" clip-path="url(#p8e4a585976)" style="fill: none; stroke-dasharray: 0.75,1.2375; stroke-dashoffset: 0; stroke: #1862ab; stroke-width: 0.75"/>
+" clip-path="url(#p8e4a585976)" style="fill: none; stroke-dasharray: 0.75,1.2375; stroke-dashoffset: 0; stroke: #1862ab; stroke-width: 0.75"/>
+" clip-path="url(#p8e4a585976)" style="fill: none; stroke-dasharray: 0.75,1.2375; stroke-dashoffset: 0; stroke: #1862ab; stroke-width: 0.75"/>
@@ -1305,7 +1223,7 @@ z
" style="fill: #ffffff"/>
-
+
+" clip-path="url(#p421b054dd2)" style="fill: #fbb4ae"/>
+" clip-path="url(#p421b054dd2)" style="fill: #b3cde3"/>
+" clip-path="url(#p421b054dd2)" style="fill: #ccebc5"/>
+" clip-path="url(#p421b054dd2)" style="fill: #decbe4"/>
+" clip-path="url(#p421b054dd2)" style="fill: #fed9a6"/>
+" clip-path="url(#p421b054dd2)" style="fill: #ffffcc"/>
+" clip-path="url(#p421b054dd2)" style="fill: #e5d8bd"/>
+" clip-path="url(#p421b054dd2)" style="fill: #fddaec"/>
+" clip-path="url(#p421b054dd2)" style="fill: #f2f2f2"/>
-
-
+
@@ -1397,7 +1315,7 @@ z
-
+
@@ -1412,7 +1330,7 @@ z
-
+
@@ -1427,7 +1345,7 @@ z
-
+
@@ -1442,7 +1360,7 @@ z
-
+
@@ -1457,7 +1375,7 @@ z
-
+
@@ -1677,10 +1595,10 @@ z
-
+
-
+
diff --git a/src/barplot.rs b/src/barplot.rs
index 6c5de17..08ff2b8 100644
--- a/src/barplot.rs
+++ b/src/barplot.rs
@@ -322,8 +322,8 @@ mod tests {
let yy = [5, 4, 3, 2, 1, 0, 1, 2, 3, 4];
let mut bar = Barplot::new();
bar.draw(&xx, &yy);
- let b: &str = "x=np.array([0,1,2,3,4,5,6,7,8,9,],dtype=float)\n\
- y=np.array([5,4,3,2,1,0,1,2,3,4,],dtype=float)\n\
+ let b: &str = "x=np.array([0,1,2,3,4,5,6,7,8,9,])\n\
+ y=np.array([5,4,3,2,1,0,1,2,3,4,])\n\
p=plt.bar(x,y)\n";
assert_eq!(bar.buffer, b);
bar.clear_buffer();
@@ -342,10 +342,10 @@ mod tests {
.set_with_text("center")
.set_extra("edgecolor='black'")
.draw(&xx, &yy);
- let b: &str = "x=np.array([0,1,2,3,4,5,6,7,8,9,],dtype=float)\n\
- y=np.array([5,4,3,2,1,0,1,2,3,4,],dtype=float)\n\
+ let b: &str = "x=np.array([0,1,2,3,4,5,6,7,8,9,])\n\
+ y=np.array([5,4,3,2,1,0,1,2,3,4,])\n\
colors=['red','green',]\n\
- bottom=np.array([1,2,3,],dtype=float)\n\
+ bottom=np.array([1,2,3,])\n\
p=plt.bar(x,y\
,label=r'LABEL'\
,color=colors\
@@ -366,7 +366,7 @@ mod tests {
let mut bar = Barplot::new();
bar.draw_with_str(&xx, &yy);
let b: &str = "x=['one','two','three',]\n\
- y=np.array([1,2,3,],dtype=float)\n\
+ y=np.array([1,2,3,])\n\
p=plt.bar(x,y)\n";
assert_eq!(bar.buffer, b);
}
@@ -384,9 +384,9 @@ mod tests {
.set_extra("edgecolor='black'")
.draw_with_str(&xx, &yy);
let b: &str = "x=['one','two','three',]\n\
- y=np.array([1,2,3,],dtype=float)\n\
+ y=np.array([1,2,3,])\n\
colors=['red','green',]\n\
- bottom=np.array([1,2,3,],dtype=float)\n\
+ bottom=np.array([1,2,3,])\n\
p=plt.bar(x,y\
,label=r'LABEL'\
,color=colors\
diff --git a/src/boxplot.rs b/src/boxplot.rs
index fb6da66..8af2b82 100644
--- a/src/boxplot.rs
+++ b/src/boxplot.rs
@@ -365,7 +365,7 @@ mod tests {
];
let mut boxes = Boxplot::new();
boxes.draw_mat(&x);
- let b: &str = "x=np.array([[1,2,3,4,5,],[2,3,4,5,6,],[3,4,5,6,7,],[4,5,6,7,8,],[5,6,7,8,9,],[6,7,8,9,10,],],dtype=float)\n\
+ let b: &str = "x=np.array([[1,2,3,4,5,],[2,3,4,5,6,],[3,4,5,6,7,],[4,5,6,7,8,],[5,6,7,8,9,],[6,7,8,9,10,],])\n\
p=plt.boxplot(x)\n";
assert_eq!(boxes.buffer, b);
boxes.clear_buffer();
@@ -392,7 +392,7 @@ mod tests {
.set_positions(&[1.0, 2.0, 3.0, 4.0, 5.0])
.set_width(0.5)
.draw_mat(&x);
- let b: &str = "x=np.array([[1,2,3,4,5,],[2,3,4,5,6,],[3,4,5,6,7,],[4,5,6,7,8,],[5,6,7,8,9,],[6,7,8,9,10,],],dtype=float)\n\
+ let b: &str = "x=np.array([[1,2,3,4,5,],[2,3,4,5,6,],[3,4,5,6,7,],[4,5,6,7,8,],[5,6,7,8,9,],[6,7,8,9,10,],])\n\
positions=[1,2,3,4,5,]\n\
p=plt.boxplot(x,sym=r'b+',vert=False,whis=1.5,positions=positions,widths=0.5,showfliers=False)\n";
assert_eq!(boxes.buffer, b);
diff --git a/src/canvas.rs b/src/canvas.rs
index 37abd64..dbe22bf 100644
--- a/src/canvas.rs
+++ b/src/canvas.rs
@@ -1,5 +1,7 @@
use super::{GraphMaker, StrError};
-use crate::AsMatrix;
+use crate::conversions::{matrix_to_array, vector_to_array};
+use crate::{AsMatrix, AsVector};
+use num_traits::Num;
use std::fmt::Write;
/// Defines the poly-curve code
@@ -148,6 +150,19 @@ pub struct Canvas {
// options
stop_clip: bool, // Stop clipping features within margins
+ shading: bool, // Shading for 3D surfaces (currently used only in draw_triangles_3d). Default = true
+
+ // options for glyph 3D
+ glyph_line_width: f64, // Line width for 3D glyphs
+ glyph_size: f64, // Size for 3D glyphs
+ glyph_color_x: String, // Color for X axis of 3D glyphs
+ glyph_color_y: String, // Color for X axis of 3D glyphs
+ glyph_color_z: String, // Color for X axis of 3D glyphs
+ glyph_label_x: String, // Label for X axis of 3D glyphs
+ glyph_label_y: String, // Label for X axis of 3D glyphs
+ glyph_label_z: String, // Label for X axis of 3D glyphs
+ glyph_label_color: String, // Color for labels of 3D glyphs (overrides individual axis colors)
+ glyph_bbox_opt: String, // Python options for the dictionary setting the bounding box of 3D glyphs' text
// buffer
buffer: String, // buffer
@@ -178,6 +193,18 @@ impl Canvas {
alt_text_rotation: 45.0,
// options
stop_clip: false,
+ shading: true,
+ // options for glyph 3D
+ glyph_line_width: 2.0,
+ glyph_size: 1.0,
+ glyph_color_x: "red".to_string(),
+ glyph_color_y: "green".to_string(),
+ glyph_color_z: "blue".to_string(),
+ glyph_label_x: "X".to_string(),
+ glyph_label_y: "Y".to_string(),
+ glyph_label_z: "Z".to_string(),
+ glyph_label_color: String::new(),
+ glyph_bbox_opt: "boxstyle='circle,pad=0.1',facecolor='white',edgecolor='None'".to_string(),
// buffer
buffer: String::new(),
}
@@ -186,7 +213,7 @@ impl Canvas {
/// Draws arc (2D only)
pub fn draw_arc(&mut self, xc: T, yc: T, r: T, ini_angle: T, fin_angle: T)
where
- T: std::fmt::Display,
+ T: std::fmt::Display + Num,
{
let opt = self.options_shared();
write!(
@@ -201,7 +228,7 @@ impl Canvas {
/// Draws arrow (2D only)
pub fn draw_arrow(&mut self, xi: T, yi: T, xf: T, yf: T)
where
- T: std::fmt::Display,
+ T: std::fmt::Display + Num,
{
let opt_shared = self.options_shared();
let opt_arrow = self.options_arrow();
@@ -220,7 +247,7 @@ impl Canvas {
/// Draws circle (2D only)
pub fn draw_circle(&mut self, xc: T, yc: T, r: T)
where
- T: std::fmt::Display,
+ T: std::fmt::Display + Num,
{
let opt = self.options_shared();
write!(
@@ -232,6 +259,76 @@ impl Canvas {
.unwrap();
}
+ /// Draws triangles (2D only)
+ ///
+ /// Using
+ pub fn draw_triangles<'a, T, U, C>(&mut self, xx: &'a T, yy: &'a T, connectivity: &'a C) -> &mut Self
+ where
+ T: AsVector<'a, U>,
+ U: 'a + std::fmt::Display + Num,
+ C: AsMatrix<'a, usize>,
+ {
+ vector_to_array(&mut self.buffer, "xx", xx);
+ vector_to_array(&mut self.buffer, "yy", yy);
+ matrix_to_array(&mut self.buffer, "triangles", connectivity);
+ let opt = self.options_triangles();
+ write!(&mut self.buffer, "plt.triplot(xx,yy,triangles{})\n", &opt).unwrap();
+ self
+ }
+
+ /// Draws triangles (3D only)
+ ///
+ /// Using
+ ///
+ /// Note: There is no way to set shading and facecolor at the same time.
+ pub fn draw_triangles_3d<'a, T, U, C>(&mut self, xx: &'a T, yy: &'a T, zz: &'a T, connectivity: &'a C) -> &mut Self
+ where
+ T: AsVector<'a, U>,
+ U: 'a + std::fmt::Display + Num,
+ C: AsMatrix<'a, usize>,
+ {
+ // write arrays
+ vector_to_array(&mut self.buffer, "xx", xx);
+ vector_to_array(&mut self.buffer, "yy", yy);
+ vector_to_array(&mut self.buffer, "zz", zz);
+ matrix_to_array(&mut self.buffer, "triangles", connectivity);
+
+ // Issue when setting facecolor directly:
+ //
+ // In matplotlib 3.6+, passing facecolors as a parameter directly to plot_trisurf() doesn't work.
+ // The solution is:
+ // * Create the surface first with shade=False
+ // * Then use set_facecolor() to apply the colors after the surface is created
+ //
+ // Also, there is no way to set shading and facecolor at the same time.
+
+ // get options without facecolor
+ let opt = self.options_triangles_3d();
+
+ // write Python command
+ let shade = if self.shading { "True" } else { "False" };
+ write!(
+ &mut self.buffer,
+ "poly_collection=ax3d().plot_trisurf(xx,yy,zz,triangles=triangles,shade={}{})\n",
+ shade, &opt
+ )
+ .unwrap();
+
+ // set facecolor if specified
+ if self.face_color != "" {
+ write!(
+ &mut self.buffer,
+ "colors=np.array(['{}']*len(triangles))\n\
+ poly_collection.set_facecolor(colors)\n",
+ self.face_color
+ )
+ .unwrap();
+ }
+
+ // done
+ self
+ }
+
/// Begins drawing a polycurve (straight segments, quadratic Bezier, and cubic Bezier) (2D only)
///
/// # Warning
@@ -251,7 +348,7 @@ impl Canvas {
/// Afterwards, you must call [Canvas::polycurve_end] when finishing adding points.
pub fn polycurve_add(&mut self, x: T, y: T, code: PolyCode) -> &mut Self
where
- T: std::fmt::Display,
+ T: std::fmt::Display + Num,
{
let keyword = match code {
PolyCode::MoveTo => "MOVETO",
@@ -364,7 +461,7 @@ impl Canvas {
/// otherwise Python/Matplotlib will fail.
pub fn polyline_3d_add(&mut self, x: T, y: T, z: T) -> &mut Self
where
- T: std::fmt::Display,
+ T: std::fmt::Display + Num,
{
write!(&mut self.buffer, "[{},{},{}],", x, y, z).unwrap();
self
@@ -391,7 +488,7 @@ impl Canvas {
pub fn draw_polyline<'a, T, U>(&mut self, points: &'a T, closed: bool)
where
T: AsMatrix<'a, U>,
- U: 'a + std::fmt::Display,
+ U: 'a + std::fmt::Display + Num,
{
let (npoint, ndim) = points.size();
if npoint < 2 {
@@ -442,7 +539,10 @@ impl Canvas {
}
/// Draws a rectangle
- pub fn draw_rectangle(&mut self, x: f64, y: f64, width: f64, height: f64) -> &mut Self {
+ pub fn draw_rectangle(&mut self, x: T, y: T, width: T, height: T) -> &mut Self
+ where
+ T: std::fmt::Display + Num,
+ {
let opt = self.options_shared();
write!(
&mut self.buffer,
@@ -455,14 +555,71 @@ impl Canvas {
}
/// Draws a text in a 2D graph
- pub fn draw_text(&mut self, x: f64, y: f64, label: &str) -> &mut Self {
- self.text(2, &[x, y, 0.0], label, false);
+ pub fn draw_text(&mut self, x: T, y: T, label: &str) -> &mut Self
+ where
+ T: std::fmt::Display + Num,
+ {
+ self.text(2, &[x, y, T::zero()], label, false);
self
}
/// Draws an alternative text in a 2D graph
- pub fn draw_alt_text(&mut self, x: f64, y: f64, label: &str) -> &mut Self {
- self.text(2, &[x, y, 0.0], label, true);
+ pub fn draw_alt_text(&mut self, x: T, y: T, label: &str) -> &mut Self
+ where
+ T: std::fmt::Display + Num,
+ {
+ self.text(2, &[x, y, T::zero()], label, true);
+ self
+ }
+
+ /// Draws a 3D glyph at position (x,y,z) to indicate the direction of the X-Y-Z axes
+ pub fn draw_glyph_3d(&mut self, x: T, y: T, z: T) -> &mut Self
+ where
+ T: std::fmt::Display + Num,
+ {
+ let size = self.glyph_size;
+ let lx = &self.glyph_label_x;
+ let ly = &self.glyph_label_y;
+ let lz = &self.glyph_label_z;
+ let lw = self.glyph_line_width;
+ let r = &self.glyph_color_x;
+ let g = &self.glyph_color_y;
+ let b = &self.glyph_color_z;
+ let tr = if self.glyph_label_color == "" {
+ &self.glyph_color_x
+ } else {
+ &self.glyph_label_color
+ };
+ let tg = if self.glyph_label_color == "" {
+ &self.glyph_color_y
+ } else {
+ &self.glyph_label_color
+ };
+ let tb = if self.glyph_label_color == "" {
+ &self.glyph_color_z
+ } else {
+ &self.glyph_label_color
+ };
+ write!(
+ &mut self.buffer,
+ "plt.gca().plot([{x},{x}+{size}],[{y},{y}],[{z},{z}],color='{r}',linewidth={lw})\n\
+ plt.gca().plot([{x},{x}],[{y},{y}+{size}],[{z},{z}],color='{g}',linewidth={lw})\n\
+ plt.gca().plot([{x},{x}],[{y},{y}],[{z},{z}+{size}],color='{b}',linewidth={lw})\n\
+ tx=plt.gca().text({x}+{size},{y},{z},'{lx}',color='{tr}',ha='center',va='center')\n\
+ ty=plt.gca().text({x},{y}+{size},{z},'{ly}',color='{tg}',ha='center',va='center')\n\
+ tz=plt.gca().text({x},{y},{z}+{size},'{lz}',color='{tb}',ha='center',va='center')\n"
+ )
+ .unwrap();
+ if self.glyph_bbox_opt != "" {
+ write!(
+ &mut self.buffer,
+ "tx.set_bbox(dict({}))\n\
+ ty.set_bbox(dict({}))\n\
+ tz.set_bbox(dict({}))\n",
+ self.glyph_bbox_opt, self.glyph_bbox_opt, self.glyph_bbox_opt
+ )
+ .unwrap();
+ }
self
}
@@ -747,6 +904,122 @@ impl Canvas {
self
}
+ /// Sets shading for 3D surfaces (currently used only in draw_triangles_3d)
+ ///
+ /// Note: Shading is disabled if facecolor is non-empty.
+ ///
+ /// Default = true
+ pub fn set_shading(&mut self, flag: bool) -> &mut Self {
+ self.shading = flag;
+ self
+ }
+
+ /// Sets the line width used when drawing 3D glyphs
+ pub fn set_glyph_line_width(&mut self, width: f64) -> &mut Self {
+ self.glyph_line_width = width;
+ self
+ }
+
+ /// Sets the size (axis length) of 3D glyphs
+ pub fn set_glyph_size(&mut self, size: f64) -> &mut Self {
+ self.glyph_size = size;
+ self
+ }
+
+ /// Sets the color of the X axis in 3D glyphs
+ pub fn set_glyph_color_x(&mut self, color: &str) -> &mut Self {
+ self.glyph_color_x = String::from(color);
+ self
+ }
+
+ /// Sets the color of the Y axis in 3D glyphs
+ pub fn set_glyph_color_y(&mut self, color: &str) -> &mut Self {
+ self.glyph_color_y = String::from(color);
+ self
+ }
+
+ /// Sets the color of the Z axis in 3D glyphs
+ pub fn set_glyph_color_z(&mut self, color: &str) -> &mut Self {
+ self.glyph_color_z = String::from(color);
+ self
+ }
+
+ /// Sets the label used for the X axis in 3D glyphs
+ pub fn set_glyph_label_x(&mut self, label: &str) -> &mut Self {
+ self.glyph_label_x = String::from(label);
+ self
+ }
+
+ /// Sets the label used for the Y axis in 3D glyphs
+ pub fn set_glyph_label_y(&mut self, label: &str) -> &mut Self {
+ self.glyph_label_y = String::from(label);
+ self
+ }
+
+ /// Sets the label used for the Z axis in 3D glyphs
+ pub fn set_glyph_label_z(&mut self, label: &str) -> &mut Self {
+ self.glyph_label_z = String::from(label);
+ self
+ }
+
+ /// Sets a color to override the default label colors in 3D glyphs
+ ///
+ /// The default colors are the same as the axis colors.
+ pub fn set_glyph_label_color(&mut self, label_clr: &str) -> &mut Self {
+ self.glyph_label_color = String::from(label_clr);
+ self
+ }
+
+ /// Sets the Python dictionary string defining the bounding box of 3D glyphs
+ ///
+ /// Note: The setting of the bounding box here is different than the on implement in `Text`.
+ /// Here, all options for the Python whole dictionary must be provided, for example
+ /// (default string):
+ ///
+ /// ```text
+ /// "boxstyle='circle,pad=0.1',facecolor='white',edgecolor='None'"
+ /// ```
+ pub fn set_glyph_bbox(&mut self, bbox_dict: &str) -> &mut Self {
+ self.glyph_bbox_opt = String::from(bbox_dict);
+ self
+ }
+
+ /// Returns options for triangles (2D only)
+ fn options_triangles(&self) -> String {
+ let mut opt = String::new();
+ if self.edge_color != "" {
+ write!(&mut opt, ",color='{}'", self.edge_color).unwrap();
+ }
+ if self.line_width > 0.0 {
+ write!(&mut opt, ",linewidth={}", self.line_width).unwrap();
+ }
+ if self.line_style != "" {
+ write!(&mut opt, ",linestyle='{}'", self.line_style).unwrap();
+ }
+ if self.stop_clip {
+ write!(&mut opt, ",clip_on=False").unwrap();
+ }
+ opt
+ }
+
+ /// Returns shared options
+ fn options_triangles_3d(&self) -> String {
+ let mut opt = String::new();
+ if self.edge_color != "" {
+ write!(&mut opt, ",edgecolor='{}'", self.edge_color).unwrap();
+ }
+ if self.line_width > 0.0 {
+ write!(&mut opt, ",linewidth={}", self.line_width).unwrap();
+ }
+ if self.line_style != "" {
+ write!(&mut opt, ",linestyle='{}'", self.line_style).unwrap();
+ }
+ if self.stop_clip {
+ write!(&mut opt, ",clip_on=False").unwrap();
+ }
+ opt
+ }
+
/// Returns shared options
fn options_shared(&self) -> String {
let mut opt = String::new();
@@ -838,7 +1111,10 @@ impl Canvas {
}
/// Draws 2D or 3D line
- fn line(&mut self, ndim: usize, a: &[f64; 3], b: &[f64; 3]) {
+ fn line(&mut self, ndim: usize, a: &[T; 3], b: &[T; 3])
+ where
+ T: std::fmt::Display,
+ {
if ndim == 2 {
write!(
&mut self.buffer,
@@ -858,7 +1134,10 @@ impl Canvas {
}
/// Draws 2D or 3D text
- fn text(&mut self, ndim: usize, a: &[f64; 3], txt: &str, alternative: bool) {
+ fn text(&mut self, ndim: usize, a: &[T; 3], txt: &str, alternative: bool)
+ where
+ T: std::fmt::Display,
+ {
let opt = if alternative {
self.options_alt_text()
} else {
@@ -1033,6 +1312,28 @@ mod tests {
assert_eq!(opt, ",color='red',linewidth=5,linestyle=':'");
}
+ #[test]
+ fn glyph_setters_work() {
+ let mut canvas = Canvas::new();
+ canvas
+ .set_glyph_line_width(4.5)
+ .set_glyph_size(2.0)
+ .set_glyph_color_x("orange")
+ .set_glyph_color_y("cyan")
+ .set_glyph_color_z("magenta")
+ .set_glyph_label_x("Ux")
+ .set_glyph_label_y("Uy")
+ .set_glyph_label_z("Uz");
+ assert_eq!(canvas.glyph_line_width, 4.5);
+ assert_eq!(canvas.glyph_size, 2.0);
+ assert_eq!(canvas.glyph_color_x, "orange");
+ assert_eq!(canvas.glyph_color_y, "cyan");
+ assert_eq!(canvas.glyph_color_z, "magenta");
+ assert_eq!(canvas.glyph_label_x, "Ux");
+ assert_eq!(canvas.glyph_label_y, "Uy");
+ assert_eq!(canvas.glyph_label_z, "Uz");
+ }
+
#[test]
fn line_works() {
let mut canvas = Canvas::new();
diff --git a/src/contour.rs b/src/contour.rs
index 109806b..1a65e81 100644
--- a/src/contour.rs
+++ b/src/contour.rs
@@ -536,11 +536,11 @@ mod tests {
let y = vec![vec![-0.5, -0.5, -0.5], vec![0.0, 0.0, 0.0], vec![0.5, 0.5, 0.5]];
let z = vec![vec![0.50, 0.25, 0.50], vec![0.25, 0.00, 0.25], vec![0.50, 0.25, 0.50]];
contour.draw(&x, &y, &z);
- let b: &str = "x=np.array([[-0.5,0,0.5,],[-0.5,0,0.5,],[-0.5,0,0.5,],],dtype=float)\n\
- y=np.array([[-0.5,-0.5,-0.5,],[0,0,0,],[0.5,0.5,0.5,],],dtype=float)\n\
- z=np.array([[0.5,0.25,0.5,],[0.25,0,0.25,],[0.5,0.25,0.5,],],dtype=float)\n\
+ let b: &str = "x=np.array([[-0.5,0,0.5,],[-0.5,0,0.5,],[-0.5,0,0.5,],])\n\
+ y=np.array([[-0.5,-0.5,-0.5,],[0,0,0,],[0.5,0.5,0.5,],])\n\
+ z=np.array([[0.5,0.25,0.5,],[0.25,0,0.25,],[0.5,0.25,0.5,],])\n\
colors=['#f00','#0f0','#00f',]\n\
- levels=np.array([0.25,0.5,1,],dtype=float)\n\
+ levels=np.array([0.25,0.5,1,])\n\
cf=plt.contourf(x,y,z,colors=colors,levels=levels)\n\
cl=plt.contour(x,y,z,colors=['black'],levels=levels)\n\
plt.clabel(cl,inline=True)\n\
diff --git a/src/conversions.rs b/src/conversions.rs
index c8923ff..98a5ae4 100644
--- a/src/conversions.rs
+++ b/src/conversions.rs
@@ -37,7 +37,7 @@ where
for i in 0..m {
write!(buf, "{},", vector.vec_at(i)).unwrap();
}
- write!(buf, "],dtype=float)\n").unwrap();
+ write!(buf, "])\n").unwrap();
}
/// Generates a nested Python list
@@ -56,6 +56,26 @@ where
write!(buf, "]\n").unwrap();
}
+/// Generates a 3-deep nested Python list
+pub(crate) fn generate_nested_list_3(buf: &mut String, name: &str, data: &Vec>>)
+where
+ T: std::fmt::Display + Num,
+{
+ write!(buf, "{}=[", name).unwrap();
+ for row in data.into_iter() {
+ write!(buf, "[").unwrap();
+ for val in row.into_iter() {
+ write!(buf, "[",).unwrap();
+ for v in val.into_iter() {
+ write!(buf, "{},", v).unwrap();
+ }
+ write!(buf, "],").unwrap();
+ }
+ write!(buf, "],").unwrap();
+ }
+ write!(buf, "]\n").unwrap();
+}
+
/// Converts a matrix to a 2D NumPy array
pub(crate) fn matrix_to_array<'a, T, U>(buf: &mut String, name: &str, matrix: &'a T)
where
@@ -71,14 +91,14 @@ where
}
write!(buf, "],").unwrap();
}
- write!(buf, "],dtype=float)\n").unwrap();
+ write!(buf, "])\n").unwrap();
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#[cfg(test)]
mod tests {
- use super::{generate_list, generate_list_quoted, generate_nested_list, matrix_to_array, vector_to_array};
+ use super::*;
#[test]
fn generate_list_works() {
@@ -125,9 +145,9 @@ mod tests {
vector_to_array(&mut buf, "z", &z);
assert_eq!(
buf,
- "x=np.array([0.1,0.2,0.3,],dtype=float)\n\
- y=np.array([1,2,3,],dtype=float)\n\
- z=np.array([10,20,30,],dtype=float)\n"
+ "x=np.array([0.1,0.2,0.3,])\n\
+ y=np.array([1,2,3,])\n\
+ z=np.array([10,20,30,])\n"
);
}
@@ -139,6 +159,17 @@ mod tests {
assert_eq!(buf, "a=[[1,2,3,],[4,5,],[6,7,8,9,],]\n");
}
+ #[test]
+ fn generate_nested_list_3_works() {
+ let mut buf = String::new();
+ let a = vec![
+ vec![vec![1.0, 0.0, 0.0, 1.0], vec![0.0, 1.0, 0.0, 1.0]], // Row 0: Red, Green
+ vec![vec![0.0, 0.0, 1.0, 1.0], vec![1.0, 1.0, 1.0, 0.5]], // Row 1: Blue, White (semi-transparent)
+ ];
+ generate_nested_list_3(&mut buf, "a", &a);
+ assert_eq!(buf, "a=[[[1,0,0,1,],[0,1,0,1,],],[[0,0,1,1,],[1,1,1,0.5,],],]\n");
+ }
+
#[test]
fn matrix_to_array_works() {
let mut buf = String::new();
@@ -150,9 +181,9 @@ mod tests {
matrix_to_array(&mut buf, "c", &c);
assert_eq!(
buf,
- "a=np.array([[1,2,3,],[4,5,6,],[7,8,9,],],dtype=float)\n\
- b=np.array([[1,2,3,],[4,5,6,],[7,8,9,],],dtype=float)\n\
- c=np.array([[1,2,3,],[4,5,6,],[7,8,9,],],dtype=float)\n"
+ "a=np.array([[1,2,3,],[4,5,6,],[7,8,9,],])\n\
+ b=np.array([[1,2,3,],[4,5,6,],[7,8,9,],])\n\
+ c=np.array([[1,2,3,],[4,5,6,],[7,8,9,],])\n"
);
}
}
diff --git a/src/curve.rs b/src/curve.rs
index 8d9f647..92f78b2 100644
--- a/src/curve.rs
+++ b/src/curve.rs
@@ -216,7 +216,7 @@ impl Curve {
/// otherwise Python/Matplotlib will fail.
pub fn points_add(&mut self, x: T, y: T) -> &mut Self
where
- T: std::fmt::Display,
+ T: std::fmt::Display + Num,
{
write!(&mut self.buffer, "[{},{}],", x, y).unwrap();
self
@@ -253,7 +253,7 @@ impl Curve {
/// otherwise Python/Matplotlib will fail.
pub fn points_3d_add(&mut self, x: T, y: T, z: T) -> &mut Self
where
- T: std::fmt::Display,
+ T: std::fmt::Display + Num,
{
write!(&mut self.buffer, "[{},{},{}],", x, y, z).unwrap();
self
@@ -624,8 +624,8 @@ mod tests {
let mut curve = Curve::new();
curve.set_label("the-curve");
curve.draw(x, y);
- let b: &str = "x=np.array([1,2,3,4,5,],dtype=float)\n\
- y=np.array([1,4,9,16,25,],dtype=float)\n\
+ let b: &str = "x=np.array([1,2,3,4,5,])\n\
+ y=np.array([1,4,9,16,25,])\n\
plt.plot(x,y,label=r'the-curve')\n";
assert_eq!(curve.buffer, b);
curve.clear_buffer();
@@ -640,9 +640,9 @@ mod tests {
let mut curve = Curve::new();
curve.set_label("the-curve");
curve.draw_3d(x, y, z);
- let b: &str = "x=np.array([1,2,3,4,5,],dtype=float)\n\
- y=np.array([1,4,9,16,25,],dtype=float)\n\
- z=np.array([0,0,0,1,1,],dtype=float)\n\
+ let b: &str = "x=np.array([1,2,3,4,5,])\n\
+ y=np.array([1,4,9,16,25,])\n\
+ z=np.array([0,0,0,1,1,])\n\
ax3d().plot(x,y,z,label=r'the-curve')\n";
assert_eq!(curve.buffer, b);
}
diff --git a/src/fill_between.rs b/src/fill_between.rs
new file mode 100644
index 0000000..aec305d
--- /dev/null
+++ b/src/fill_between.rs
@@ -0,0 +1,173 @@
+use super::{vector_to_array, AsVector, GraphMaker};
+use num_traits::Num;
+use std::fmt::Write;
+
+/// Fills the area between two curves
+///
+/// # Examples
+///
+/// ```
+/// use plotpy::{Curve, FillBetween, Plot, StrError, linspace};
+///
+/// fn main() -> Result<(), StrError> {
+/// // data and curve
+/// let x = linspace(-1.0, 2.0, 21);
+/// let y: Vec<_> = x.iter().map(|&x| x * x).collect();
+/// let mut curve = Curve::new();
+/// curve.set_line_color("black").draw(&x, &y);
+///
+/// // draw area between curve and x-axis
+/// // (note that we have to use "y1" as variable name for the curve)
+/// let mut fb = FillBetween::new();
+/// fb.set_where("y1>=0.5").set_extra("alpha=0.5").draw(&x, &y, None);
+///
+/// // add curve and fb to plot
+/// let mut plot = Plot::new();
+/// plot.add(&curve).add(&fb);
+///
+/// // save figure
+/// plot.save("/tmp/plotpy/doc_tests/doc_fill_between.svg")?;
+/// Ok(())
+/// }
+/// ```
+///
+/// 
+pub struct FillBetween {
+ where_condition: String,
+ facecolor: String,
+ interpolate: bool,
+ extra: String,
+ buffer: String,
+}
+
+impl FillBetween {
+ /// Allocates a new instance
+ pub fn new() -> Self {
+ FillBetween {
+ where_condition: String::new(),
+ facecolor: String::new(),
+ interpolate: false,
+ extra: String::new(),
+ buffer: String::new(),
+ }
+ }
+
+ /// Draws the filled area between two curves
+ ///
+ /// * `x` - x values
+ /// * `y1` - y values of the first curve
+ /// * `y2` - optional y values of the second curve. If None, fills area between y1 and x-axis
+ ///
+ pub fn draw<'a, T, U>(&mut self, x: &'a T, y1: &'a T, y2: Option<&'a T>)
+ where
+ T: AsVector<'a, U>,
+ U: 'a + std::fmt::Display + Num,
+ {
+ let opt = self.options();
+ vector_to_array(&mut self.buffer, "x", x);
+ vector_to_array(&mut self.buffer, "y1", y1);
+ match y2 {
+ Some(y2) => {
+ vector_to_array(&mut self.buffer, "y2", y2);
+ write!(&mut self.buffer, "plt.fill_between(x,y1,y2{})\n", &opt).unwrap();
+ }
+ None => {
+ write!(&mut self.buffer, "plt.fill_between(x,y1{})\n", &opt).unwrap();
+ }
+ }
+ }
+
+ /// Sets the condition to select the area to be filled.
+ ///
+ /// For example: "y2>=y1" or "y2<=y1"
+ ///
+ /// **WARNING:** `condition` must use `y1` and `y2` as variable names for the two curves.
+ pub fn set_where(&mut self, condition: &str) -> &mut Self {
+ self.where_condition = condition.to_string();
+ self
+ }
+
+ /// Sets the face color of the filled area.
+ pub fn set_facecolor(&mut self, color: &str) -> &mut Self {
+ self.facecolor = color.to_string();
+ self
+ }
+
+ /// Calculates the actual intersection point and extend the filled region up to this point.
+ ///
+ /// From :
+ ///
+ /// "This option is only relevant if where is used and the two curves are crossing each other. Semantically,
+ /// `where` is often used for y1 > y2 or similar. By default, the nodes of the polygon defining the filled
+ /// region will only be placed at the positions in the x array. Such a polygon cannot describe the above
+ /// semantics close to the intersection. The x-sections containing the intersection are simply clipped."
+ ///
+ /// Default is false.
+ pub fn set_interpolate(&mut self, interpolate: bool) -> &mut Self {
+ self.interpolate = interpolate;
+ self
+ }
+
+ /// Fills the area between two curves
+ ///
+ /// **WARNING:** `where_condition` must use `y1` and `y2` as variable names for the two curves.
+ /// For example:
+ ///
+ /// ```text
+ /// curve.fill_between(x, y1, y2, "y2>=y1", "#ffaabb", true, "");
+ /// curve.fill_between(x, y1, y2, "y2>=y1", "#ffaabb", true, "");
+ /// curve.fill_between(x, y1, y2b, "y2<=y1", "#c1e3ff", true, "");
+ /// ```
+ ///
+ /// **Note:** This method does not use the options of the Curve object.
+ ///
+ /// See more options in
+ pub fn set_extra(&mut self, extra: &str) -> &mut Self {
+ self.extra = extra.to_string();
+ self
+ }
+
+ /// Returns the options
+ fn options(&self) -> String {
+ let mut opt = String::new();
+ if self.facecolor != "" {
+ write!(&mut opt, ",facecolor='{}'", self.facecolor).unwrap();
+ }
+ if self.where_condition != "" {
+ write!(&mut opt, ",where={}", self.where_condition).unwrap();
+ }
+ if self.interpolate {
+ write!(&mut opt, ",interpolate=True").unwrap();
+ }
+ if self.extra != "" {
+ write!(&mut opt, ",{}", self.extra).unwrap();
+ }
+ opt
+ }
+}
+
+impl GraphMaker for FillBetween {
+ fn get_buffer<'a>(&'a self) -> &'a String {
+ &self.buffer
+ }
+ fn clear_buffer(&mut self) {
+ self.buffer.clear();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+#[cfg(test)]
+mod tests {
+ use super::FillBetween;
+
+ #[test]
+ fn new_works() {
+ let fill_between = FillBetween::new();
+ assert_eq!(fill_between.where_condition, "");
+ assert_eq!(fill_between.facecolor, "");
+ assert_eq!(fill_between.interpolate, false);
+ assert_eq!(fill_between.extra, "");
+ assert_eq!(fill_between.buffer.len(), 0);
+ }
+}
diff --git a/src/image.rs b/src/image.rs
index 4e82385..903dfd2 100644
--- a/src/image.rs
+++ b/src/image.rs
@@ -1,4 +1,4 @@
-use super::{matrix_to_array, AsMatrix, GraphMaker};
+use super::{generate_nested_list_3, matrix_to_array, AsMatrix, GraphMaker};
use num_traits::Num;
use std::fmt::Write;
@@ -55,6 +55,12 @@ impl Image {
}
/// (imshow) Displays data as an image
+ ///
+ /// # Arguments
+ ///
+ /// * `data` - 2D matrix-like data structure
+ ///
+ /// See
pub fn draw<'a, T, U>(&mut self, data: &'a T)
where
T: AsMatrix<'a, U>,
@@ -65,6 +71,23 @@ impl Image {
write!(&mut self.buffer, "plt.imshow(data{})\n", &opt).unwrap();
}
+ /// (imshow) Displays data as an image with RGB or RGB(A) values
+ ///
+ /// # Arguments
+ ///
+ /// * `data` - 3D vector with shape (height, width, 3) for RGB or (height, width, 4) for RGBA
+ /// The inner-most vector contains the color channels.
+ ///
+ /// See
+ pub fn draw_rgb_or_rgba(&mut self, data: &Vec>>)
+ where
+ T: std::fmt::Display + Num,
+ {
+ generate_nested_list_3(&mut self.buffer, "data", data);
+ let opt = self.options();
+ write!(&mut self.buffer, "plt.imshow(data{})\n", &opt).unwrap();
+ }
+
/// Sets the colormap index
///
/// Options:
@@ -143,7 +166,7 @@ mod tests {
let xx = [[1, 2], [3, 2]];
let mut img = Image::new();
img.set_colormap_index(0).set_colormap_name("terrain").draw(&xx);
- let b: &str = "data=np.array([[1,2,],[3,2,],],dtype=float)\n\
+ let b: &str = "data=np.array([[1,2,],[3,2,],])\n\
plt.imshow(data,cmap=plt.get_cmap('terrain'))\n";
assert_eq!(img.buffer, b);
img.clear_buffer();
diff --git a/src/lib.rs b/src/lib.rs
index 970814c..e6b590c 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -84,6 +84,7 @@ mod contour;
mod conversions;
mod curve;
mod fileio;
+mod fill_between;
mod histogram;
mod image;
mod inset_axes;
@@ -107,6 +108,7 @@ pub use contour::*;
use conversions::*;
pub use curve::*;
use fileio::*;
+pub use fill_between::*;
pub use histogram::*;
pub use image::*;
pub use inset_axes::*;
diff --git a/src/plot.rs b/src/plot.rs
index f2aa1a6..6be9dc7 100644
--- a/src/plot.rs
+++ b/src/plot.rs
@@ -514,6 +514,23 @@ impl Plot {
self
}
+ /// Sets an option to hide/show the 3D grid and panes (make them transparent)
+ ///
+ /// **Important:** This function must be called after adding all 3D surfaces/curves to the plot.
+ pub fn set_hide_3d_grid(&mut self, hide: bool) -> &mut Self {
+ if hide {
+ self.buffer.push_str(
+ "plt.gca().xaxis.pane.set_color((1.0, 1.0, 1.0, 0.0))\n\
+ plt.gca().yaxis.pane.set_color((1.0, 1.0, 1.0, 0.0))\n\
+ plt.gca().zaxis.pane.set_color((1.0, 1.0, 1.0, 0.0))\n\
+ plt.gca().grid(False)\n",
+ );
+ } else {
+ self.buffer.push_str("plt.gca().grid(True)\n");
+ }
+ self
+ }
+
/// Sets axes limits
pub fn set_range_3d(&mut self, xmin: f64, xmax: f64, ymin: f64, ymax: f64, zmin: f64, zmax: f64) -> &mut Self {
write!(
@@ -1043,13 +1060,24 @@ impl Plot {
///
/// # Input
///
- /// * `elev` -- is the elevation angle in the z plane
+ /// * `elevation` -- is the elevation angle in the z plane
/// * `azimuth` -- is the azimuth angle in the x,y plane
- pub fn set_camera(&mut self, elev: f64, azimuth: f64) -> &mut Self {
+ ///
+ /// | view plane | elev | azim |
+ /// |------------|------|------|
+ /// | XY | 90 | -90 |
+ /// | XZ | 0 | -90 |
+ /// | YZ | 0 | 0 |
+ /// | -XY | -90 | 90 |
+ /// | -XZ | 0 | 90 |
+ /// | -YZ | 0 | 180 |
+ ///
+ /// See
+ pub fn set_camera(&mut self, elevation: f64, azimuth: f64) -> &mut Self {
write!(
&mut self.buffer,
"plt.gca().view_init(elev={},azim={})\n",
- elev, azimuth
+ elevation, azimuth
)
.unwrap();
self
diff --git a/src/surface.rs b/src/surface.rs
index f75538d..252d5f6 100644
--- a/src/surface.rs
+++ b/src/surface.rs
@@ -600,9 +600,9 @@ mod tests {
let y = vec![vec![-0.5, -0.5, -0.5], vec![0.0, 0.0, 0.0], vec![0.5, 0.5, 0.5]];
let z = vec![vec![0.50, 0.25, 0.50], vec![0.25, 0.00, 0.25], vec![0.50, 0.25, 0.50]];
surface.draw(&x, &y, &z);
- let b: &str = "x=np.array([[-0.5,0,0.5,],[-0.5,0,0.5,],[-0.5,0,0.5,],],dtype=float)\n\
- y=np.array([[-0.5,-0.5,-0.5,],[0,0,0,],[0.5,0.5,0.5,],],dtype=float)\n\
- z=np.array([[0.5,0.25,0.5,],[0.25,0,0.25,],[0.5,0.25,0.5,],],dtype=float)\n\
+ let b: &str = "x=np.array([[-0.5,0,0.5,],[-0.5,0,0.5,],[-0.5,0,0.5,],])\n\
+ y=np.array([[-0.5,-0.5,-0.5,],[0,0,0,],[0.5,0.5,0.5,],])\n\
+ z=np.array([[0.5,0.25,0.5,],[0.25,0,0.25,],[0.5,0.25,0.5,],])\n\
sf=ax3d().plot_surface(x,y,z,cmap=plt.get_cmap('bwr'))\n\
ax3d().plot_wireframe(x,y,z,color='black')\n\
cb=plt.colorbar(sf)\n\
diff --git a/src/text.rs b/src/text.rs
index 88da6a9..e92e138 100644
--- a/src/text.rs
+++ b/src/text.rs
@@ -1,4 +1,5 @@
use super::GraphMaker;
+use num_traits::Num;
use std::fmt::Write;
/// Creates text to be added to a plot
@@ -45,7 +46,7 @@ pub struct Text {
align_horizontal: String, // Horizontal alignment
align_vertical: String, // Vertical alignment
fontsize: f64, // Font size
- rotation: f64, // Text rotation
+ rotation: Option, // Text rotation
// bounding box
bbox: bool, // Use bounding box
@@ -67,7 +68,7 @@ impl Text {
align_horizontal: String::new(),
align_vertical: String::new(),
fontsize: 0.0,
- rotation: 0.0,
+ rotation: None,
bbox: false,
bbox_facecolor: String::new(),
bbox_edgecolor: String::new(),
@@ -79,7 +80,10 @@ impl Text {
}
/// Draws text
- pub fn draw(&mut self, x: f64, y: f64, message: &str) {
+ pub fn draw(&mut self, x: T, y: T, message: &str)
+ where
+ T: std::fmt::Display + Num,
+ {
let opt = self.options();
write!(&mut self.buffer, "t=plt.text({},{},r'{}'{})\n", x, y, message, &opt).unwrap();
if self.bbox {
@@ -89,7 +93,10 @@ impl Text {
}
/// Draws text in 3D plot
- pub fn draw_3d(&mut self, x: f64, y: f64, z: f64, message: &str) {
+ pub fn draw_3d(&mut self, x: T, y: T, z: T, message: &str)
+ where
+ T: std::fmt::Display + Num,
+ {
let opt = self.options();
write!(
&mut self.buffer,
@@ -131,9 +138,11 @@ impl Text {
self
}
- /// Sets the text rotation (2D only)
+ /// Sets the text rotation angle in degrees (2D only)
+ ///
+ /// See
pub fn set_rotation(&mut self, rotation: f64) -> &mut Self {
- self.rotation = rotation;
+ self.rotation = Some(rotation);
self
}
@@ -210,8 +219,8 @@ impl Text {
if self.fontsize > 0.0 {
write!(&mut opt, ",fontsize={}", self.fontsize).unwrap();
}
- if self.rotation > 0.0 {
- write!(&mut opt, ",rotation={}", self.rotation).unwrap();
+ if let Some(rotation) = self.rotation {
+ write!(&mut opt, ",rotation={}", rotation).unwrap();
}
if self.extra != "" {
write!(&mut opt, ",{}", self.extra).unwrap();
@@ -259,7 +268,7 @@ mod tests {
assert_eq!(text.align_horizontal.len(), 0);
assert_eq!(text.align_vertical.len(), 0);
assert_eq!(text.fontsize, 0.0);
- assert_eq!(text.rotation, 0.0);
+ assert_eq!(text.rotation, None);
assert_eq!(text.buffer.len(), 0);
}
diff --git a/tests/test_canvas.rs b/tests/test_canvas.rs
index 715c94e..f9b5bfb 100644
--- a/tests/test_canvas.rs
+++ b/tests/test_canvas.rs
@@ -427,3 +427,137 @@ fn test_canvas_rectangle_and_text() -> Result<(), StrError> {
assert!(n > 530 && n < 600);
Ok(())
}
+
+#[test]
+fn test_canvas_draw_triangles() -> Result<(), StrError> {
+ // point coordinates (two triangles in a square)
+ let xx = &[0.0, 1.0, 1.0, 0.0];
+ let yy = &[0.0, 0.0, 1.0, 1.0];
+ let triangles = &[[0, 1, 3], [1, 2, 3]];
+
+ // canvas
+ let mut canvas = Canvas::new();
+
+ // configurations
+ canvas
+ .set_edge_color("#cd9806ff")
+ .set_line_width(2.0)
+ .set_line_style("--");
+
+ // draw triangles
+ canvas.draw_triangles(xx, yy, triangles);
+
+ // add canvas to plot
+ let mut plot = Plot::new();
+ plot.add(&canvas);
+
+ // save figure
+ let path = Path::new(OUT_DIR).join("integ_canvas_draw_triangles.svg");
+ plot.set_equal_axes(true).set_show_errors(true).save(&path)?;
+
+ // check number of lines
+ let file = File::open(path).map_err(|_| "cannot open file")?;
+ let buffered = BufReader::new(file);
+ let lines_iter = buffered.lines();
+ let n = lines_iter.count();
+ assert!(n > 410 && n < 470);
+ Ok(())
+}
+
+#[test]
+fn test_canvas_draw_triangles_3d() -> Result<(), StrError> {
+ // point coordinates (one tetrahedron)
+ let xx = &[0.0, 1.0, 0.0, 0.0];
+ let yy = &[0.0, 0.0, 1.0, 0.0];
+ let zz = &[0.0, 0.0, 0.0, 1.0];
+ let triangles = &[[0, 1, 2], [0, 1, 3], [0, 2, 3], [1, 2, 3]];
+
+ // canvas
+ let mut canvas_shading = Canvas::new();
+ let mut canvas_facecolor = Canvas::new();
+
+ // configurations
+ canvas_shading
+ .set_edge_color("#17d8e9ff")
+ .set_line_width(4.0)
+ .set_line_style("--");
+ canvas_facecolor
+ .set_face_color("#de3163")
+ .set_edge_color("#17d8e9ff")
+ .set_line_width(4.0);
+
+ // draw triangles
+ canvas_shading.draw_triangles_3d(xx, yy, zz, triangles);
+ canvas_facecolor.draw_triangles_3d(xx, yy, zz, triangles);
+
+ // add canvas to plot
+ let mut plot = Plot::new();
+ plot.set_subplot_3d(1, 2, 1).add(&canvas_shading);
+ plot.set_subplot_3d(1, 2, 2).add(&canvas_facecolor);
+
+ // save figure
+ let path = Path::new(OUT_DIR).join("integ_canvas_draw_triangles_3d.svg");
+ plot.set_equal_axes(true)
+ .set_show_errors(true)
+ .set_figure_size_points(800.0, 400.0)
+ .save(&path)?;
+
+ // check number of lines
+ let file = File::open(path).map_err(|_| "cannot open file")?;
+ let buffered = BufReader::new(file);
+ let lines_iter = buffered.lines();
+ let n = lines_iter.count();
+ assert!(n > 1120 && n < 1180);
+ Ok(())
+}
+
+#[test]
+fn test_canvas_glyph_3d_and_hide_3d_grid() -> Result<(), StrError> {
+ let y = 0.5;
+ const W: f64 = 2.0;
+ const H: f64 = 1.0;
+ let mut canvas = Canvas::new();
+ canvas.set_edge_color("orange").set_line_width(5.0);
+ canvas
+ .polyline_3d_begin()
+ .polyline_3d_add(W, y, 0.0)
+ .polyline_3d_add(0.0, y, 0.0)
+ .polyline_3d_add(0.0, y, H)
+ .polyline_3d_add(W, y, H)
+ .polyline_3d_add(W, y, 0.0) // close
+ .polyline_3d_end();
+
+ canvas.set_glyph_line_width(4.0).draw_glyph_3d(1.5, -2.5, -1.0);
+
+ canvas
+ .set_glyph_label_color("black")
+ .set_glyph_line_width(4.0)
+ .draw_glyph_3d(1.5, -0.5, -1.0);
+
+ canvas
+ .set_glyph_label_color("")
+ .set_glyph_color_x("#7a1581ff")
+ .set_glyph_color_y("#c87208ff")
+ .set_glyph_color_z("#12827cff")
+ .set_glyph_line_width(4.0)
+ .set_glyph_bbox("boxstyle='circle,pad=0.2',facecolor='white',edgecolor='black'")
+ .draw_glyph_3d(-1.0, -2.5, -1.0);
+
+ // add canvas to plot
+ let mut plot = Plot::new();
+ plot.add(&canvas).set_hide_3d_grid(true).set_camera(30.0, 30.0);
+
+ // save figure
+ let path = Path::new(OUT_DIR).join("integ_canvas_glyph_3d_and_hide_3d_grid.svg");
+ plot.set_equal_axes(true).set_show_errors(true);
+ plot.save(&path)?;
+ // plot.save_and_show(&path)?;
+
+ // check number of lines
+ let file = File::open(path).map_err(|_| "cannot open file")?;
+ let buffered = BufReader::new(file);
+ let lines_iter = buffered.lines();
+ let n = lines_iter.count();
+ assert!(n > 850 && n < 910);
+ Ok(())
+}
diff --git a/tests/test_fill_between.rs b/tests/test_fill_between.rs
new file mode 100644
index 0000000..a6163d7
--- /dev/null
+++ b/tests/test_fill_between.rs
@@ -0,0 +1,93 @@
+use plotpy::{linspace, Curve, FillBetween, Plot, StrError};
+use std::fs::File;
+use std::io::{BufRead, BufReader};
+use std::path::Path;
+
+const OUT_DIR: &str = "/tmp/plotpy/integ_tests";
+
+#[test]
+fn test_fill_between_1() -> Result<(), StrError> {
+ // data
+ let x = linspace(-1.0, 2.0, 21);
+ let y1: Vec<_> = x.iter().map(|&x| x * x).collect();
+ let y2: Vec<_> = x.iter().map(|&x| x).collect();
+
+ // draw
+ let mut fb = FillBetween::new();
+ fb.draw(&x, &y1, Some(&y2));
+
+ // add fb to plot
+ let mut plot = Plot::new();
+ plot.add(&fb);
+
+ // save figure
+ let path = Path::new(OUT_DIR).join("integ_fill_between_1.svg");
+ plot.save(&path)?;
+
+ // check number of lines
+ let file = File::open(path).map_err(|_| "cannot open file")?;
+ let buffered = BufReader::new(file);
+ let lines_iter = buffered.lines();
+ let n = lines_iter.count();
+ assert!(n > 420 && n < 500);
+ Ok(())
+}
+
+#[test]
+fn test_fill_between_2() -> Result<(), StrError> {
+ // data
+ let x = linspace(-1.0, 2.0, 21);
+ let y1: Vec<_> = x.iter().map(|&x| x * x).collect();
+ let y2: Vec<_> = x.iter().map(|&x| x).collect();
+
+ // draw
+ let mut fb = FillBetween::new();
+ fb.set_interpolate(true);
+ fb.set_facecolor("#ffaabb").set_where("y1>=y2").draw(&x, &y1, Some(&y2));
+ fb.set_facecolor("#c1e3ff").set_where("y2>=y1").draw(&x, &y1, Some(&y2));
+
+ // add fb to plot
+ let mut plot = Plot::new();
+ plot.add(&fb);
+
+ // save figure
+ let path = Path::new(OUT_DIR).join("integ_fill_between_2.svg");
+ plot.save(&path)?;
+
+ // check number of lines
+ let file = File::open(path).map_err(|_| "cannot open file")?;
+ let buffered = BufReader::new(file);
+ let lines_iter = buffered.lines();
+ let n = lines_iter.count();
+ assert!(n > 450 && n < 500);
+ Ok(())
+}
+
+#[test]
+fn test_fill_between_3() -> Result<(), StrError> {
+ // data and curve
+ let x = linspace(-1.0, 2.0, 21);
+ let y: Vec<_> = x.iter().map(|&x| x * x).collect();
+ let mut curve = Curve::new();
+ curve.set_line_color("black").draw(&x, &y);
+
+ // draw
+ let mut fb = FillBetween::new();
+ fb.set_where("y1>=0.5").set_extra("alpha=0.5").draw(&x, &y, None);
+
+ // add curve and fb to plot
+ let mut plot = Plot::new();
+ plot.add(&curve).add(&fb);
+
+ // save figure
+ let path = Path::new(OUT_DIR).join("integ_fill_between_3.svg");
+ plot.save(&path)?;
+
+ // check number of lines
+ let file = File::open(path).map_err(|_| "cannot open file")?;
+ let buffered = BufReader::new(file);
+ let lines_iter = buffered.lines();
+ let n = lines_iter.count();
+ assert!(n > 500 && n < 560);
+ Ok(())
+}
diff --git a/tests/test_image.rs b/tests/test_image.rs
index 3b303c6..b5ea5b1 100644
--- a/tests/test_image.rs
+++ b/tests/test_image.rs
@@ -37,3 +37,93 @@ fn test_image_1() -> Result<(), StrError> {
assert!(c > 420 && c < 500);
Ok(())
}
+
+#[test]
+fn test_image_with_rgb() -> Result<(), StrError> {
+ let data = vec![
+ // --- Row 0 ---
+ vec![
+ vec![1.0, 0.0, 0.0], // Pixel 0,0: Red
+ vec![0.0, 1.0, 0.0], // Pixel 0,1: Green
+ vec![0.0, 0.0, 1.0], // Pixel 0,2: Blue
+ ],
+ // --- Row 1 ---
+ vec![
+ vec![1.0, 1.0, 0.0], // Pixel 1,0: Yellow
+ vec![1.0, 0.0, 1.0], // Pixel 1,1: Magenta
+ vec![0.0, 1.0, 1.0], // Pixel 1,2: Cyan
+ ],
+ // --- Row 2 ---
+ vec![
+ vec![0.5, 0.5, 0.5], // Pixel 2,0: Gray
+ vec![1.0, 1.0, 1.0], // Pixel 2,1: White
+ vec![0.0, 0.0, 0.0], // Pixel 2,2: Black
+ ],
+ ];
+
+ // image plot and options
+ let mut img = Image::new();
+ img.set_colormap_name("terrain")
+ .set_extra("alpha=0.8")
+ .draw_rgb_or_rgba(&data);
+
+ let mut plot = Plot::new();
+ plot.add(&img);
+
+ // save figure
+ let path = Path::new(OUT_DIR).join("integ_image_with_rgb.svg");
+ plot.set_show_errors(true).save(&path)?;
+
+ // check number of lines
+ let file = File::open(path).map_err(|_| "cannot open file")?;
+ let buffered = BufReader::new(file);
+ let lines_iter = buffered.lines();
+ let c = lines_iter.count();
+ assert!(c > 400 && c < 430);
+ Ok(())
+}
+
+#[test]
+fn test_image_with_rgba() -> Result<(), StrError> {
+ let data = vec![
+ // --- Row 0 ---
+ vec![
+ vec![1.0, 0.0, 0.0, 0.5], // Pixel 0,0: Red
+ vec![0.0, 1.0, 0.0, 0.5], // Pixel 0,1: Green
+ vec![0.0, 0.0, 1.0, 0.5], // Pixel 0,2: Blue
+ ],
+ // --- Row 1 ---
+ vec![
+ vec![1.0, 1.0, 0.0, 0.8], // Pixel 1,0: Yellow
+ vec![1.0, 0.0, 1.0, 0.8], // Pixel 1,1: Magenta
+ vec![0.0, 1.0, 1.0, 0.8], // Pixel 1,2: Cyan
+ ],
+ // --- Row 2 ---
+ vec![
+ vec![0.5, 0.5, 0.5, 0.2], // Pixel 2,0: Gray
+ vec![1.0, 1.0, 1.0, 0.2], // Pixel 2,1: White
+ vec![0.0, 0.0, 0.0, 0.2], // Pixel 2,2: Black
+ ],
+ ];
+
+ // image plot and options
+ let mut img = Image::new();
+ img.set_colormap_name("terrain")
+ .set_extra("alpha=0.8")
+ .draw_rgb_or_rgba(&data);
+
+ let mut plot = Plot::new();
+ plot.add(&img);
+
+ // save figure
+ let path = Path::new(OUT_DIR).join("integ_image_with_rgba.svg");
+ plot.set_show_errors(true).save(&path)?;
+
+ // check number of lines
+ let file = File::open(path).map_err(|_| "cannot open file")?;
+ let buffered = BufReader::new(file);
+ let lines_iter = buffered.lines();
+ let c = lines_iter.count();
+ assert!(c > 400 && c < 430);
+ Ok(())
+}
diff --git a/tests/test_surface.rs b/tests/test_surface.rs
index 05821de..aa59623 100644
--- a/tests/test_surface.rs
+++ b/tests/test_surface.rs
@@ -31,7 +31,7 @@ fn test_surface() -> Result<(), StrError> {
// save figure
let path = Path::new(OUT_DIR).join("integ_surface.svg");
- plot.save(&path)?;
+ plot.set_hide_3d_grid(true).save(&path)?;
// check number of lines
let file = File::open(path).map_err(|_| "cannot open file")?;
diff --git a/tests/test_text.rs b/tests/test_text.rs
index d3e5a8f..89db7f4 100644
--- a/tests/test_text.rs
+++ b/tests/test_text.rs
@@ -48,6 +48,35 @@ fn test_text() -> Result<(), StrError> {
Ok(())
}
+#[test]
+fn test_text_negative_rotation() -> Result<(), StrError> {
+ // text object and options
+ let mut text = Text::new();
+
+ // draw text
+ text.set_align_horizontal("center")
+ .set_align_vertical("center")
+ .set_fontsize(20.0)
+ .set_rotation(-30.0)
+ .draw(0.5, 0.5, "NEGATIVE ROTATION");
+
+ // add text to plot
+ let mut plot = Plot::new();
+ plot.add(&text).set_range(0.0, 1.0, 0.0, 1.0);
+
+ // save figure
+ let path = Path::new(OUT_DIR).join("integ_text_negative_rotation.svg");
+ plot.set_show_errors(true).save(&path)?;
+
+ // check number of lines
+ let file = File::open(path).map_err(|_| "cannot open file")?;
+ let buffered = BufReader::new(file);
+ let lines_iter = buffered.lines();
+ let n = lines_iter.count();
+ assert!(n > 570 && n < 620);
+ Ok(())
+}
+
#[test]
fn test_text_3d() -> Result<(), StrError> {
// text object and options