use crate::hatch::Hatch;
use crate::scale::ScaleKind;
use crate::series::{
bin_observations, build_boxplot_series, staircase, AreaOpts, AreaSeries, BarOpts, BarSeries,
BoxPlotOpts, ContourOpts, ContourSeries, ErrorBarOpts, ErrorBarSeries, HeatmapSeries,
HistogramOpts, HistogramSeries, HlineOpts, HlineSeries, LineOpts, LineSeries, Normalize,
Origin, PolygonOpts, PolygonSeries, QuiverOpts, QuiverSeries, ScatterOpts, ScatterSeries,
Series, StemOpts, StemSeries, VlineOpts, VlineSeries,
};
use crate::strokes::Stroke;
use crate::theme::Theme;
use crate::title_block::TitleBlock;
#[derive(Debug, Clone, Copy)]
pub enum PaperSize {
A4,
A4Landscape,
A5,
A5Landscape,
Letter,
LetterLandscape,
Legal,
LegalLandscape,
Square,
Custom(f64, f64),
}
impl PaperSize {
pub fn dimensions(self) -> (f64, f64) {
match self {
PaperSize::A4 => (794.0, 1123.0),
PaperSize::A4Landscape => (1123.0, 794.0),
PaperSize::A5 => (559.0, 794.0),
PaperSize::A5Landscape => (794.0, 559.0),
PaperSize::Letter => (816.0, 1056.0),
PaperSize::LetterLandscape => (1056.0, 816.0),
PaperSize::Legal => (816.0, 1344.0),
PaperSize::LegalLandscape => (1344.0, 816.0),
PaperSize::Square => (600.0, 600.0),
PaperSize::Custom(w, h) => (w, h),
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum LegendPosition {
TopRight,
TopLeft,
BottomRight,
BottomLeft,
Manual(f64, f64),
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum Projection {
#[default]
None,
Polar,
Mercator,
Equirect,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum Clip {
#[default]
Rect,
Circle,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum AxesStyle {
#[default]
Both,
None,
}
#[derive(Debug, Clone, Copy)]
pub enum ColorbarPosition {
Right,
Left,
Bottom,
Manual(f64, f64),
}
#[derive(Debug, Clone)]
pub(crate) struct ColorbarConfig {
pub position: ColorbarPosition,
pub label: Option<String>,
pub levels: usize,
pub ramp: Option<Vec<Hatch>>,
pub range: Option<(f64, f64)>,
}
#[derive(Debug, Clone)]
pub(crate) enum Annotation {
Text {
x: f64,
y: f64,
text: String,
font_size: Option<f64>,
anchor: TextAnchor,
halo: bool,
},
Arrow {
from: (f64, f64),
to: (f64, f64),
},
}
#[derive(Debug, Clone, Copy, Default)]
pub enum TextAnchor {
#[default]
Start,
Middle,
End,
}
impl TextAnchor {
pub(crate) fn as_str(self) -> &'static str {
match self {
TextAnchor::Start => "start",
TextAnchor::Middle => "middle",
TextAnchor::End => "end",
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub enum GridStyle {
None,
#[default]
Major,
Both,
}
#[derive(Debug, Clone)]
pub(crate) struct LegendConfig {
pub position: LegendPosition,
pub title: Option<String>,
}
#[derive(Debug, Clone, Copy)]
pub enum Limit {
Auto,
Manual(f64, f64),
}
#[derive(Debug, Clone)]
pub struct Figure {
pub(crate) width: f64,
pub(crate) height: f64,
pub(crate) margins: (f64, f64, f64, f64),
pub(crate) title: Option<String>,
pub(crate) subtitle: Option<String>,
pub(crate) xlabel: Option<String>,
pub(crate) ylabel: Option<String>,
pub(crate) xlim: Limit,
pub(crate) ylim: Limit,
pub(crate) xscale: ScaleKind,
pub(crate) yscale: ScaleKind,
pub(crate) grid: GridStyle,
pub(crate) series: Vec<Series>,
pub(crate) annotations: Vec<Annotation>,
pub(crate) legend: Option<LegendConfig>,
pub(crate) colorbar: Option<ColorbarConfig>,
pub(crate) title_block: Option<TitleBlock>,
pub(crate) theme: Theme,
pub(crate) projection: Projection,
pub(crate) clip: Clip,
pub(crate) axes_style: AxesStyle,
}
impl Figure {
pub fn new() -> Self {
let (w, h) = PaperSize::LetterLandscape.dimensions();
Self {
width: w,
height: h,
margins: (80.0, 60.0, 80.0, 90.0),
title: None,
subtitle: None,
xlabel: None,
ylabel: None,
xlim: Limit::Auto,
ylim: Limit::Auto,
xscale: ScaleKind::Linear,
yscale: ScaleKind::Linear,
grid: GridStyle::Major,
series: Vec::new(),
annotations: Vec::new(),
legend: None,
colorbar: None,
title_block: None,
theme: Theme::report_1972(),
projection: Projection::None,
clip: Clip::Rect,
axes_style: AxesStyle::Both,
}
}
pub fn size(mut self, paper: PaperSize) -> Self {
let (w, h) = paper.dimensions();
self.width = w;
self.height = h;
self
}
pub fn dimensions(mut self, width: f64, height: f64) -> Self {
self.width = width;
self.height = height;
self
}
pub fn theme(mut self, theme: Theme) -> Self {
self.theme = theme;
self
}
pub fn margins(mut self, top: f64, right: f64, bottom: f64, left: f64) -> Self {
self.margins = (top, right, bottom, left);
self
}
pub fn title(mut self, t: impl Into<String>) -> Self {
self.title = Some(t.into());
self
}
pub fn subtitle(mut self, t: impl Into<String>) -> Self {
self.subtitle = Some(t.into());
self
}
pub fn xlabel(mut self, t: impl Into<String>) -> Self {
self.xlabel = Some(t.into());
self
}
pub fn ylabel(mut self, t: impl Into<String>) -> Self {
self.ylabel = Some(t.into());
self
}
pub fn xlim(mut self, lo: f64, hi: f64) -> Self {
self.xlim = Limit::Manual(lo, hi);
self
}
pub fn ylim(mut self, lo: f64, hi: f64) -> Self {
self.ylim = Limit::Manual(lo, hi);
self
}
pub fn xlog(mut self) -> Self {
self.xscale = ScaleKind::Log;
self
}
pub fn ylog(mut self) -> Self {
self.yscale = ScaleKind::Log;
self
}
pub fn grid(mut self, g: GridStyle) -> Self {
self.grid = g;
self
}
pub fn line<F>(mut self, xs: &[f64], ys: &[f64], f: F) -> Self
where
F: FnOnce(LineOpts) -> LineOpts,
{
let o = f(LineOpts::default());
self.series.push(Series::Line(LineSeries {
xs: xs.to_vec(),
ys: ys.to_vec(),
label: o.label,
stroke: o.stroke,
stroke_width: o.stroke_width,
markers: o.markers,
marker: o.marker,
marker_size: o.marker_size,
}));
self
}
pub fn scatter<F>(mut self, xs: &[f64], ys: &[f64], f: F) -> Self
where
F: FnOnce(ScatterOpts) -> ScatterOpts,
{
let o = f(ScatterOpts::default());
self.series.push(Series::Scatter(ScatterSeries {
xs: xs.to_vec(),
ys: ys.to_vec(),
label: o.label,
marker: o.marker,
marker_size: o.marker_size,
stroke_width: o.stroke_width,
}));
self
}
pub fn bar<F, S>(mut self, categories: &[S], values: &[f64], f: F) -> Self
where
F: FnOnce(BarOpts) -> BarOpts,
S: AsRef<str>,
{
let o = f(BarOpts::default());
self.series.push(Series::Bar(BarSeries {
categories: categories.iter().map(|c| c.as_ref().to_string()).collect(),
values: values.to_vec(),
label: o.label,
hatch: o.hatch,
group: o.group,
stroke_width: o.stroke_width,
}));
self
}
pub fn area<F>(mut self, xs: &[f64], ys: &[f64], f: F) -> Self
where
F: FnOnce(AreaOpts) -> AreaOpts,
{
let o = f(AreaOpts::default());
self.series.push(Series::Area(AreaSeries {
xs: xs.to_vec(),
ys: ys.to_vec(),
label: o.label,
hatch: o.hatch,
baseline: o.baseline,
stroke: o.stroke,
stroke_width: o.stroke_width,
}));
self
}
pub fn histogram<F>(mut self, observations: &[f64], f: F) -> Self
where
F: FnOnce(HistogramOpts) -> HistogramOpts,
{
let o = f(HistogramOpts::default());
let label = o.label.clone();
let hatch = o.hatch;
let stroke_width = o.stroke_width;
let normalize = o.normalize;
let (edges, values) = bin_observations(observations, &o);
if matches!(normalize, Normalize::Cmf) {
let (xs, ys) = staircase(&edges, &values);
self.series.push(Series::Line(LineSeries {
xs,
ys,
label,
stroke: Some(Stroke::Solid),
stroke_width,
markers: false,
marker: None,
marker_size: None,
}));
} else {
self.series.push(Series::Histogram(HistogramSeries {
bin_edges: edges,
values,
label,
hatch,
stroke_width,
}));
}
self
}
pub fn polygon<F>(mut self, xs: &[f64], ys: &[f64], f: F) -> Self
where
F: FnOnce(PolygonOpts) -> PolygonOpts,
{
let o = f(PolygonOpts::default());
self.series.push(Series::Polygon(PolygonSeries {
xs: xs.to_vec(),
ys: ys.to_vec(),
label: o.label,
hatch: o.hatch,
stroke: o.stroke,
stroke_width: o.stroke_width,
}));
self
}
pub fn errorbar<F>(mut self, xs: &[f64], ys: &[f64], f: F) -> Self
where
F: FnOnce(ErrorBarOpts) -> ErrorBarOpts,
{
let o = f(ErrorBarOpts::default());
self.series.push(Series::ErrorBar(ErrorBarSeries {
xs: xs.to_vec(),
ys: ys.to_vec(),
yerr: o.yerr,
xerr: o.xerr,
marker: o.marker,
marker_size: o.marker_size,
cap_width: o.cap_width,
stroke_width: o.stroke_width,
label: o.label,
}));
self
}
pub fn boxplot<S, F>(mut self, samples_by_category: &[(S, Vec<f64>)], f: F) -> Self
where
S: AsRef<str>,
F: FnOnce(BoxPlotOpts) -> BoxPlotOpts,
{
let o = f(BoxPlotOpts::default());
let pairs: Vec<(String, Vec<f64>)> = samples_by_category
.iter()
.map(|(c, s)| (c.as_ref().to_string(), s.clone()))
.collect();
let series = build_boxplot_series(&pairs, o);
self.series.push(Series::BoxPlot(series));
self
}
pub fn stem<F>(mut self, xs: &[f64], ys: &[f64], f: F) -> Self
where
F: FnOnce(StemOpts) -> StemOpts,
{
let o = f(StemOpts::default());
self.series.push(Series::Stem(StemSeries {
xs: xs.to_vec(),
ys: ys.to_vec(),
baseline: o.baseline,
label: o.label,
stroke: o.stroke,
stroke_width: o.stroke_width,
marker: o.marker,
marker_size: o.marker_size,
}));
self
}
pub fn quiver<F>(mut self, xs: &[f64], ys: &[f64], us: &[f64], vs: &[f64], f: F) -> Self
where
F: FnOnce(QuiverOpts) -> QuiverOpts,
{
let o = f(QuiverOpts::default());
self.series.push(Series::Quiver(QuiverSeries {
xs: xs.to_vec(),
ys: ys.to_vec(),
us: us.to_vec(),
vs: vs.to_vec(),
scale: o.scale,
head_size: o.head_size,
label: o.label,
stroke: o.stroke,
stroke_width: o.stroke_width,
}));
self
}
pub fn contour<F>(mut self, data: Vec<Vec<f64>>, f: F) -> Self
where
F: FnOnce(ContourOpts) -> ContourOpts,
{
let rows = data.len();
let cols = data.first().map(|r| r.len()).unwrap_or(0);
let mut o = ContourOpts::default();
o.x_edges = Some((0..=cols).map(|i| i as f64).collect());
o.y_edges = Some((0..=rows).map(|i| i as f64).collect());
let o = f(o);
let levels = o.levels.unwrap_or_else(|| default_levels(&data));
self.series.push(Series::Contour(ContourSeries {
data,
x_edges: o.x_edges.unwrap(),
y_edges: o.y_edges.unwrap(),
levels,
origin: o.origin,
stroke: o.stroke,
stroke_width: o.stroke_width,
label: o.label,
}));
self
}
pub fn annotate_text(
mut self,
x: f64,
y: f64,
text: impl Into<String>,
anchor: TextAnchor,
) -> Self {
self.annotations.push(Annotation::Text {
x,
y,
text: text.into(),
font_size: None,
anchor,
halo: true,
});
self
}
pub fn annotate_text_sized(
mut self,
x: f64,
y: f64,
text: impl Into<String>,
anchor: TextAnchor,
font_size: f64,
) -> Self {
self.annotations.push(Annotation::Text {
x,
y,
text: text.into(),
font_size: Some(font_size),
anchor,
halo: true,
});
self
}
pub fn annotate_arrow(mut self, from: (f64, f64), to: (f64, f64)) -> Self {
self.annotations.push(Annotation::Arrow { from, to });
self
}
pub fn colorbar(mut self, position: ColorbarPosition) -> Self {
self.colorbar = Some(ColorbarConfig {
position,
label: None,
levels: 5,
ramp: None,
range: None,
});
self
}
pub fn colorbar_with(
mut self,
position: ColorbarPosition,
label: Option<String>,
levels: usize,
) -> Self {
self.colorbar = Some(ColorbarConfig {
position,
label,
levels,
ramp: None,
range: None,
});
self
}
pub fn projection(mut self, p: Projection) -> Self {
self.projection = p;
self
}
pub fn clip(mut self, c: Clip) -> Self {
self.clip = c;
self
}
pub fn no_axes(mut self) -> Self {
self.axes_style = AxesStyle::None;
self
}
pub fn polar(rmax: f64) -> Self {
Self::new()
.size(PaperSize::Square)
.projection(Projection::Polar)
.clip(Clip::Circle)
.no_axes()
.grid(GridStyle::None)
.xlim(-rmax, rmax)
.ylim(-rmax, rmax)
}
pub fn polar_grid(self, opts: PolarGridOpts) -> Self {
let rmax = match (self.xlim, self.ylim) {
(Limit::Manual(x0, x1), _) => x0.abs().max(x1.abs()),
(_, Limit::Manual(y0, y1)) => y0.abs().max(y1.abs()),
_ => 1.0,
};
let r_ticks = opts.r_ticks.unwrap_or_else(|| {
let step = rmax / 4.0;
(1..=4).map(|i| i as f64 * step).collect()
});
let theta_step_deg = opts.theta_step_deg;
let stroke = opts.stroke;
let sw = opts.stroke_width;
let samples = opts.samples;
let mut fig = self;
for r in &r_ticks {
let (thetas, rs) = crate::polar::circle(*r, samples);
fig = fig.line(&thetas, &rs, |s| s.stroke(stroke).stroke_width(sw));
}
let mut deg = 0i32;
while deg < 360 {
let theta = deg as f64 * std::f64::consts::PI / 180.0;
fig = fig.line(&[theta, theta], &[0.0, rmax], |s| {
s.stroke(stroke).stroke_width(sw)
});
deg += theta_step_deg;
}
if opts.labels {
let label_radius = rmax * 1.07;
let mut deg = 0i32;
while deg < 360 {
let theta = deg as f64 * std::f64::consts::PI / 180.0;
fig = fig.annotate_text_sized(
theta,
label_radius,
format!("{}°", deg),
TextAnchor::Middle,
9.0,
);
deg += theta_step_deg;
}
}
if opts.r_labels {
for r in &r_ticks {
fig = fig.annotate_text_sized(
0.0,
*r,
crate::ticks::format(*r),
TextAnchor::Start,
8.0,
);
}
}
fig
}
pub fn smith() -> Self {
Self::new()
.size(PaperSize::Square)
.clip(Clip::Circle)
.no_axes()
.grid(GridStyle::None)
.xlim(-1.08, 1.08)
.ylim(-1.08, 1.08)
}
pub fn smith_grid(self, opts: SmithGridOpts) -> Self {
let r_values = opts
.r_values
.unwrap_or_else(|| crate::smith::DEFAULT_R_VALUES.to_vec());
let x_values = opts
.x_values
.unwrap_or_else(|| crate::smith::DEFAULT_X_VALUES.to_vec());
let stroke = opts.stroke;
let sw = opts.stroke_width;
let boundary_sw = opts.boundary_stroke_width;
let samples = opts.samples;
let mut fig = self;
let (uxs, uys) = crate::smith::r_circle(0.0, samples);
fig = fig.line(&uxs, &uys, |s| {
s.stroke(Stroke::Solid).stroke_width(boundary_sw)
});
fig = fig.line(&[-1.0, 1.0], &[0.0, 0.0], |s| {
s.stroke(Stroke::Solid).stroke_width(boundary_sw)
});
for r in &r_values {
let (xs, ys) = crate::smith::r_circle(*r, samples);
fig = fig.line(&xs, &ys, |s| s.stroke(stroke).stroke_width(sw));
}
for x_mag in &x_values {
let (xs_p, ys_p) = crate::smith::x_arc(*x_mag, samples);
let (xs_n, ys_n) = crate::smith::x_arc(-*x_mag, samples);
fig = fig.line(&xs_p, &ys_p, |s| s.stroke(stroke).stroke_width(sw));
fig = fig.line(&xs_n, &ys_n, |s| s.stroke(stroke).stroke_width(sw));
}
if opts.labels {
for r in &r_values {
let x_pos = (r - 1.0) / (r + 1.0);
fig = fig.annotate_text_sized(
x_pos,
0.0,
crate::ticks::format(*r),
TextAnchor::Middle,
7.0,
);
}
for x_mag in &x_values {
let denom = x_mag * x_mag + 1.0;
let gre = (x_mag * x_mag - 1.0) / denom;
let gim_top = 2.0 * x_mag / denom;
fig = fig.annotate_text_sized(
gre,
gim_top * 1.05,
format!("+{}", crate::ticks::format(*x_mag)),
TextAnchor::Middle,
7.0,
);
fig = fig.annotate_text_sized(
gre,
-gim_top * 1.05,
format!("-{}", crate::ticks::format(*x_mag)),
TextAnchor::Middle,
7.0,
);
}
}
fig
}
pub fn basemap<F>(self, layer: crate::basemaps::Basemap, f: F) -> Self
where
F: FnOnce(crate::basemaps::BasemapOpts) -> crate::basemaps::BasemapOpts,
{
let opts = f(crate::basemaps::BasemapOpts::default());
let features = crate::basemaps::features(layer, opts.resolution);
let features = crate::basemaps::filter_features(features, &opts.only, &opts.except);
let mut fig = self;
for feature in features {
let xs: Vec<f64> = feature.points.iter().map(|p| p.0).collect();
let ys: Vec<f64> = feature.points.iter().map(|p| p.1).collect();
if feature.closed {
let stroke = opts.stroke;
let stroke_width = opts.stroke_width;
let hatch = opts.hatch;
fig = fig.polygon(&xs, &ys, |p| {
let p = p.stroke(stroke).stroke_width(stroke_width);
match hatch {
Some(h) => p.hatch(h),
None => p,
}
});
} else {
let stroke = opts.stroke;
let stroke_width = opts.stroke_width;
fig = fig.line(&xs, &ys, |s| s.stroke(stroke).stroke_width(stroke_width));
}
}
fig
}
pub fn graticule(self, opts: crate::geo::GraticuleOpts) -> Self {
let lines = crate::geo::graticule(opts);
let mut fig = self;
for (xs, ys) in &lines {
fig = fig.line(xs, ys, |s| s.stroke(Stroke::Dotted).stroke_width(0.5));
}
fig
}
pub fn heatmap<F>(mut self, data: Vec<Vec<f64>>, f: F) -> Self
where
F: FnOnce(HeatmapOpts) -> HeatmapOpts,
{
let rows = data.len();
let cols = data.first().map(|r| r.len()).unwrap_or(0);
let mut o = HeatmapOpts::default();
o.x_edges = Some((0..=cols).map(|i| i as f64).collect());
o.y_edges = Some((0..=rows).map(|i| i as f64).collect());
let o = f(o);
self.series.push(Series::Heatmap(HeatmapSeries {
data,
x_edges: o.x_edges.unwrap(),
y_edges: o.y_edges.unwrap(),
ramp: o.ramp.unwrap_or_else(|| Hatch::DEFAULT_RAMP.to_vec()),
range: o.range,
origin: o.origin,
label: o.label,
}));
self
}
pub fn hline<F>(mut self, y: f64, f: F) -> Self
where
F: FnOnce(HlineOpts) -> HlineOpts,
{
let o = f(HlineOpts::default());
self.series.push(Series::Hline(HlineSeries {
y,
label: o.label,
stroke: o.stroke.unwrap_or(Stroke::Dashed),
stroke_width: o.stroke_width.unwrap_or(self.theme.series_stroke_width),
}));
self
}
pub fn vline<F>(mut self, x: f64, f: F) -> Self
where
F: FnOnce(VlineOpts) -> VlineOpts,
{
let o = f(VlineOpts::default());
self.series.push(Series::Vline(VlineSeries {
x,
label: o.label,
stroke: o.stroke.unwrap_or(Stroke::Dashed),
stroke_width: o.stroke_width.unwrap_or(self.theme.series_stroke_width),
}));
self
}
pub fn legend(mut self, position: LegendPosition) -> Self {
self.legend = Some(LegendConfig {
position,
title: None,
});
self
}
pub fn legend_top_right(self) -> Self {
self.legend(LegendPosition::TopRight)
}
pub fn legend_top_left(self) -> Self {
self.legend(LegendPosition::TopLeft)
}
pub fn legend_bottom_right(self) -> Self {
self.legend(LegendPosition::BottomRight)
}
pub fn legend_bottom_left(self) -> Self {
self.legend(LegendPosition::BottomLeft)
}
pub fn legend_with_title(mut self, position: LegendPosition, title: impl Into<String>) -> Self {
self.legend = Some(LegendConfig {
position,
title: Some(title.into()),
});
self
}
pub fn title_block(mut self, tb: TitleBlock) -> Self {
self.title_block = Some(tb);
self
}
pub fn to_svg(&self) -> String {
crate::renderer::render(self)
}
#[cfg(feature = "gui")]
pub fn show(&self) {
crate::gui::show(self);
}
#[cfg(feature = "gui")]
pub fn show_with_options(&self, opts: crate::gui::ShowOptions) {
crate::gui::show_with_options(self, opts);
}
#[cfg(feature = "raster")]
pub fn to_pixmap(&self, width: u32, height: u32) -> Vec<u8> {
crate::raster::to_pixmap(self, width, height)
}
pub(crate) fn plot_rect(&self) -> (f64, f64, f64, f64) {
let (t, r, b, l) = self.margins;
let pw = (self.width - l - r).max(1.0);
let ph = (self.height - t - b).max(1.0);
(l, t, pw, ph)
}
}
impl Default for Figure {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Default)]
pub struct HeatmapOpts {
pub(crate) x_edges: Option<Vec<f64>>,
pub(crate) y_edges: Option<Vec<f64>>,
pub(crate) ramp: Option<Vec<Hatch>>,
pub(crate) range: Option<(f64, f64)>,
pub(crate) origin: Origin,
pub(crate) label: Option<String>,
}
impl HeatmapOpts {
pub fn x_edges(mut self, edges: Vec<f64>) -> Self {
self.x_edges = Some(edges);
self
}
pub fn y_edges(mut self, edges: Vec<f64>) -> Self {
self.y_edges = Some(edges);
self
}
pub fn ramp(mut self, ramp: Vec<Hatch>) -> Self {
self.ramp = Some(ramp);
self
}
pub fn ramp_levels(mut self, n: usize) -> Self {
self.ramp = Some(sample_ramp(n));
self
}
pub fn range(mut self, lo: f64, hi: f64) -> Self {
self.range = Some((lo, hi));
self
}
pub fn origin(mut self, origin: Origin) -> Self {
self.origin = origin;
self
}
pub fn label(mut self, s: impl Into<String>) -> Self {
self.label = Some(s.into());
self
}
}
fn sample_ramp(n: usize) -> Vec<Hatch> {
if n <= 1 {
return vec![Hatch::Crosshatch];
}
let steps = Hatch::DEFAULT_RAMP.len() - 1;
(0..n)
.map(|i| {
let idx = (i as f64 * steps as f64 / (n - 1) as f64).round() as usize;
Hatch::DEFAULT_RAMP[idx]
})
.collect()
}
fn default_levels(grid: &[Vec<f64>]) -> Vec<f64> {
let (lo, hi) = crate::hatch::extent(grid);
let step = (hi - lo) / 8.0;
(1..=7).map(|i| lo + i as f64 * step).collect()
}
#[derive(Debug, Clone)]
pub struct PolarGridOpts {
pub r_ticks: Option<Vec<f64>>,
pub theta_step_deg: i32,
pub stroke: Stroke,
pub stroke_width: f64,
pub samples: usize,
pub labels: bool,
pub r_labels: bool,
}
impl Default for PolarGridOpts {
fn default() -> Self {
Self {
r_ticks: None,
theta_step_deg: 30,
stroke: Stroke::Dotted,
stroke_width: 0.4,
samples: 120,
labels: true,
r_labels: false,
}
}
}
#[derive(Debug, Clone)]
pub struct SmithGridOpts {
pub r_values: Option<Vec<f64>>,
pub x_values: Option<Vec<f64>>,
pub stroke: Stroke,
pub stroke_width: f64,
pub boundary_stroke_width: f64,
pub samples: usize,
pub labels: bool,
}
impl Default for SmithGridOpts {
fn default() -> Self {
Self {
r_values: None,
x_values: None,
stroke: Stroke::Dotted,
stroke_width: 0.4,
boundary_stroke_width: 0.8,
samples: 120,
labels: true,
}
}
}