From f5e044ba59aae9455ddb513b04efc9b95eea58a5 Mon Sep 17 00:00:00 2001 From: Dorival Pedroso Date: Tue, 27 Jan 2026 22:02:40 +1000 Subject: [PATCH] Impl DarkMode --- .vscode/settings.json | 6 + examples/README.md | 10 + figures/integ_dark_mode_default.svg | 777 ++++++++++++++++++++++++ figures/integ_dark_mode_mathematica.svg | 777 ++++++++++++++++++++++++ figures/integ_dark_mode_mocha.svg | 777 ++++++++++++++++++++++++ figures/integ_dark_mode_nordic.svg | 777 ++++++++++++++++++++++++ src/dark_mode.rs | 180 ++++++ src/lib.rs | 2 + tests/test_dark_mode.rs | 133 ++++ 9 files changed, 3439 insertions(+) create mode 100644 figures/integ_dark_mode_default.svg create mode 100644 figures/integ_dark_mode_mathematica.svg create mode 100644 figures/integ_dark_mode_mocha.svg create mode 100644 figures/integ_dark_mode_nordic.svg create mode 100644 src/dark_mode.rs create mode 100644 tests/test_dark_mode.rs diff --git a/.vscode/settings.json b/.vscode/settings.json index ff995d8..d499842 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -27,6 +27,7 @@ "CARETRIGHTBASE", "CARETUP", "CARETUPBASE", + "Catppuccin", "cbook", "clabel", "CLOSEPOLY", @@ -45,6 +46,7 @@ "fileio", "fontsize", "fontweight", + "frameon", "gridspec", "handlelength", "handletextpad", @@ -71,6 +73,7 @@ "markevery", "MOVETO", "mplot", + "nord", "numpoints", "numpy", "patheffects", @@ -84,6 +87,7 @@ "showfliers", "stepfilled", "suptitle", + "textcolor", "TICKDOWN", "TICKLEFT", "TICKRIGHT", @@ -104,6 +108,7 @@ "xmin", "xnticks", "xrange", + "xtick", "xticklabels", "xticks", "yaxis", @@ -113,6 +118,7 @@ "ymin", "ynticks", "yrange", + "ytick", "yticklabels", "yticks", "zaxis", diff --git a/examples/README.md b/examples/README.md index 1cac750..7a09c20 100644 --- a/examples/README.md +++ b/examples/README.md @@ -11,6 +11,7 @@ Some output of integration tests are shown below. - [Canvas](#canvas) - [Contour](#contour) - [Curve](#curve) +- [DarkMode](#darkmode) - [FillBetween](#fillbetween) - [Histogram](#histogram) - [Image](#image) @@ -92,6 +93,15 @@ Some output of integration tests are shown below. ![curve](https://raw.githubusercontent.com/cpmech/plotpy/main/figures/integ_curve_twinx.svg) ![curve](https://raw.githubusercontent.com/cpmech/plotpy/main/figures/integ_curve.svg) +## DarkMode + +[test_dark_mode.rs](https://github.com/cpmech/plotpy/tree/main/tests/test_dark_mode.rs) + +![default](https://raw.githubusercontent.com/cpmech/plotpy/main/figures/integ_dark_mode_default.svg) +![mathematica](https://raw.githubusercontent.com/cpmech/plotpy/main/figures/integ_dark_mode_mathematica.svg) +![mocha](https://raw.githubusercontent.com/cpmech/plotpy/main/figures/integ_dark_mode_mocha.svg) +![nordic](https://raw.githubusercontent.com/cpmech/plotpy/main/figures/integ_dark_mode_nordic.svg) + ## FillBetween [test_fill_between.rs](https://github.com/cpmech/plotpy/tree/main/tests/test_fill_between.rs) diff --git a/figures/integ_dark_mode_default.svg b/figures/integ_dark_mode_default.svg new file mode 100644 index 0000000..d828773 --- /dev/null +++ b/figures/integ_dark_mode_default.svg @@ -0,0 +1,777 @@ + + + + + + + + 2026-01-27T21:59:42.792913 + image/svg+xml + + + Matplotlib v3.6.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/figures/integ_dark_mode_mathematica.svg b/figures/integ_dark_mode_mathematica.svg new file mode 100644 index 0000000..e17e759 --- /dev/null +++ b/figures/integ_dark_mode_mathematica.svg @@ -0,0 +1,777 @@ + + + + + + + + 2026-01-27T21:59:42.793388 + image/svg+xml + + + Matplotlib v3.6.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/figures/integ_dark_mode_mocha.svg b/figures/integ_dark_mode_mocha.svg new file mode 100644 index 0000000..850fbac --- /dev/null +++ b/figures/integ_dark_mode_mocha.svg @@ -0,0 +1,777 @@ + + + + + + + + 2026-01-27T21:59:42.792897 + image/svg+xml + + + Matplotlib v3.6.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/figures/integ_dark_mode_nordic.svg b/figures/integ_dark_mode_nordic.svg new file mode 100644 index 0000000..c0d6096 --- /dev/null +++ b/figures/integ_dark_mode_nordic.svg @@ -0,0 +1,777 @@ + + + + + + + + 2026-01-27T21:59:42.803668 + image/svg+xml + + + Matplotlib v3.6.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/dark_mode.rs b/src/dark_mode.rs new file mode 100644 index 0000000..9c5f0c7 --- /dev/null +++ b/src/dark_mode.rs @@ -0,0 +1,180 @@ +use super::GraphMaker; + +/// Implements a dark mode enabler for plots +/// +/// **Warning;** This instance must be the **first** to be added to the `Plot` object, +pub struct DarkMode { + buffer: String, +} + +impl DarkMode { + /// Allocates a new instance + /// + /// **Warning;** This instance must be the **first** to be added to the `Plot` object, + pub fn new() -> Self { + let mut dm = DarkMode { buffer: String::new() }; + dm.set_dark_background(); + dm + } + + /// Sets the Matplotlib native dark mode (dark_background) + pub fn set_dark_background(&mut self) { + self.buffer.clear(); + self.buffer.push_str("plt.style.use('dark_background')\n"); + } + + /// Sets the Mathematica-like dark mode + /// + /// **Important:** This mode requires `cycler` package in Python environment. + pub fn set_mathematica(&mut self) { + self.buffer.clear(); + self.buffer.push_str( + r#" +########### Setting dark mode: begin ########### + +from cycler import cycler + +# 1. Background and Text Colors +plt.rcParams.update({ + 'figure.facecolor': '#000000', # Pure black background + 'axes.facecolor': '#000000', # Pure black plotting area + 'text.color': '#FFFFFF', # White text + 'axes.labelcolor': '#FFFFFF', # White axis labels + 'xtick.color': '#FFFFFF', # White x-axis ticks + 'ytick.color': '#FFFFFF', # White y-axis ticks + 'axes.edgecolor': '#555555', # Muted gray spines (Mathematica style) +}) + +# 2. Mathematica 'Vibrant' Color Cycle +# These hex codes approximate the default Mathematica 10+ plot palette +mathematica_colors = [ + '#5E81B5', # Blue + '#E19C24', # Orange + '#8FB032', # Green + '#EB6238', # Red + '#9467BD', # Purple + '#8C564B', # Brown + '#E377C2' # Pink +] +plt.rcParams['axes.prop_cycle'] = cycler('color', mathematica_colors) + +# 3. Refined Details +plt.rcParams.update({ + 'grid.color': '#313244', # Surface 0 (Subtle grid) + 'legend.facecolor': '#181825', # Mantle + 'legend.edgecolor': '#313244', + 'legend.labelcolor': '#cdd6f4' +}) + +########### Setting dark mode: end ########### + +"#, + ); + } + + /// **Important:** This mode requires `cycler` package in Python environment. + pub fn set_mocha(&mut self) { + self.buffer.clear(); + self.buffer.push_str( + r#" +########### Setting dark mode: begin ########### + +from cycler import cycler + +# 1. Background and Base Colors (Catppuccin Mocha) +plt.rcParams.update({ + 'figure.facecolor': '#11111b', # Crust (Deepest dark) + 'axes.facecolor': '#1e1e2e', # Base (Slightly lighter for contrast) + 'savefig.facecolor': '#11111b', + 'text.color': '#cdd6f4', # Text + 'axes.labelcolor': '#cdd6f4', # Text + 'xtick.color': '#7f849c', # Overlay 1 (Muted gray) + 'ytick.color': '#7f849c', + 'axes.edgecolor': '#45475a', # Surface 1 +}) + +# 2. Catppuccin Mocha Palette Color Cycle +# Selecting the most vibrant "flavor" accents +mocha_colors = [ + '#89b4fa', # Blue + '#fab387', # Peach + '#a6e3a1', # Green + '#f38ba8', # Red + '#cba6f7', # Mauve + '#94e2d5', # Teal + '#f9e2af' # Yellow +] +plt.rcParams['axes.prop_cycle'] = cycler('color', mocha_colors) + +# 3. Refined Details +plt.rcParams.update({ + 'grid.color': '#313244', # Surface 0 (Subtle grid) + 'legend.facecolor': '#181825', # Mantle + 'legend.edgecolor': '#313244', + 'legend.labelcolor': '#cdd6f4' +}) + +########### Setting dark mode: end ########### + +"#, + ); + } + + /// Sets an alternative dark mode ("Nordic Night" or "Material Dark") + /// + /// **Important:** This mode requires `cycler` package in Python environment. + pub fn set_nordic(&mut self) { + self.buffer.clear(); + self.buffer.push_str( + r#" +########### Setting dark mode: begin ########### + +from cycler import cycler + +# 1. Background and Base Colors +plt.rcParams.update({ + 'figure.facecolor': '#2E3440', # Soft charcoal + 'axes.facecolor': '#2E3440', # Match axes to figure + 'savefig.facecolor': '#2E3440', # Ensure saved images are dark + 'text.color': '#D8DEE9', # Off-white/Silver text + 'axes.labelcolor': '#D8DEE9', + 'xtick.color': '#4C566A', # Muted gray ticks + 'ytick.color': '#4C566A', + 'axes.edgecolor': '#4C566A', # Muted borders +}) + +# 2. Nord Palette Color Cycle (Modern Pastels) +nord_colors = [ + '#88C0D0', # Frost Blue + '#81A1C1', # Glacial Blue + '#BF616A', # Soft Red + '#D08770', # Orange + '#EBCB8B', # Yellow + '#A3BE8C', # Sage Green + '#B48EAD' # Muted Purple +] +plt.rcParams['axes.prop_cycle'] = cycler('color', nord_colors) + +# 3. Refined Details +plt.rcParams.update({ + 'grid.color': '#3B4252', # Darker gray grid lines + 'legend.facecolor': '#181825', # Mantle + 'legend.edgecolor': '#313244', + 'legend.labelcolor': '#D8DEE9' +}) + +########### Setting dark mode: end ########### + +"#, + ); + } +} + +impl GraphMaker for DarkMode { + fn get_buffer<'a>(&'a self) -> &'a String { + &self.buffer + } + fn clear_buffer(&mut self) { + self.buffer.clear(); + } +} diff --git a/src/lib.rs b/src/lib.rs index cec1ed0..bf890c2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -83,6 +83,7 @@ mod constants; mod contour; mod conversions; mod curve; +mod dark_mode; mod fileio; mod fill_between; mod histogram; @@ -108,6 +109,7 @@ pub use constants::*; pub use contour::*; use conversions::*; pub use curve::*; +pub use dark_mode::*; use fileio::*; pub use fill_between::*; pub use histogram::*; diff --git a/tests/test_dark_mode.rs b/tests/test_dark_mode.rs new file mode 100644 index 0000000..a20448f --- /dev/null +++ b/tests/test_dark_mode.rs @@ -0,0 +1,133 @@ +use plotpy::{Curve, DarkMode, 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_dark_mode_default() -> Result<(), StrError> { + // curve + let x = [1.0, 2.0, 3.0, 4.0]; + let y = [1.0, 4.0, 9.0, 16.0]; + let mut curve = Curve::new(); + curve.set_label("curve").draw(&x, &y); + + // dark mode enabler + let dm = DarkMode::new(); + + // plot + let mut plot = Plot::new(); + plot.add(&dm).add(&curve); + + // save figure + let path = Path::new(OUT_DIR).join("integ_dark_mode_default.svg"); + plot.legend() + .grid_and_labels("x", "y") + .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 > 700 && n < 830); + Ok(()) +} + +#[test] +fn test_dark_mode_mathematica() -> Result<(), StrError> { + // curve + let x = [1.0, 2.0, 3.0, 4.0]; + let y = [1.0, 4.0, 9.0, 16.0]; + let mut curve = Curve::new(); + curve.set_label("curve").draw(&x, &y); + + // dark mode enabler + let mut dm = DarkMode::new(); + dm.set_mathematica(); + + // plot + let mut plot = Plot::new(); + plot.add(&dm).add(&curve); + + // save figure + let path = Path::new(OUT_DIR).join("integ_dark_mode_mathematica.svg"); + plot.legend() + .grid_and_labels("x", "y") + .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 > 700 && n < 830); + Ok(()) +} + +#[test] +fn test_dark_mode_mocha() -> Result<(), StrError> { + // curve + let x = [1.0, 2.0, 3.0, 4.0]; + let y = [1.0, 4.0, 9.0, 16.0]; + let mut curve = Curve::new(); + curve.set_label("curve").draw(&x, &y); + + // dark mode enabler + let mut dm = DarkMode::new(); + dm.set_mocha(); + + // plot + let mut plot = Plot::new(); + plot.add(&dm).add(&curve); + + // save figure + let path = Path::new(OUT_DIR).join("integ_dark_mode_mocha.svg"); + plot.legend() + .grid_and_labels("x", "y") + .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 > 700 && n < 830); + Ok(()) +} + +#[test] +fn test_dark_mode_nordic() -> Result<(), StrError> { + // curve + let x = [1.0, 2.0, 3.0, 4.0]; + let y = [1.0, 4.0, 9.0, 16.0]; + let mut curve = Curve::new(); + curve.set_label("curve").draw(&x, &y); + + // dark mode enabler + let mut dm = DarkMode::new(); + dm.set_nordic(); + + // plot + let mut plot = Plot::new(); + plot.add(&dm).add(&curve); + + // save figure + let path = Path::new(OUT_DIR).join("integ_dark_mode_nordic.svg"); + plot.legend() + .grid_and_labels("x", "y") + .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 > 700 && n < 830); + Ok(()) +}