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) ![image](https://raw.githubusercontent.com/cpmech/plotpy/main/figures/integ_image_1.svg) 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 @@ + + + + + + + + 2025-10-19T22:00:25.916726 + image/svg+xml + + + Matplotlib v3.6.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + 2025-11-16T10:47:17.318281 + image/svg+xml + + + Matplotlib v3.6.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + 2025-11-16T10:47:46.552337 + image/svg+xml + + + Matplotlib v3.6.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + 2025-10-19T22:01:10.807643 + image/svg+xml + + + Matplotlib v3.6.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + 2025-10-19T22:01:12.442011 + image/svg+xml + + + Matplotlib v3.6.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + 2025-10-19T22:01:14.230449 + image/svg+xml + + + Matplotlib v3.6.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + 2025-11-17T15:09:35.644389 + image/svg+xml + + + Matplotlib v3.6.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + 2025-11-17T15:10:14.912436 + image/svg+xml + + + Matplotlib v3.6.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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(()) +/// } +/// ``` +/// +/// ![doc_fill_between.svg](https://raw.githubusercontent.com/cpmech/plotpy/main/figures/doc_fill_between.svg) +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