1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
//! [`quiver`](Axes::quiver): a 2D vector-field arrow plot.
//!
//! Mirrors matplotlib's `quiver` in its simplest form: at each base point
//! `(x, y)` an arrow is drawn pointing along the vector `(u, v)`. Each arrow is
//! a [`Line2D`] shaft plus a filled [`Patch`] triangle arrowhead, so both fold
//! into autoscaling via [`data_limits`](Axes::data_limits). Arrow lengths are
//! auto-scaled deterministically from the vector magnitudes (no RNG), so a
//! longer vector always yields a longer arrow.
use crate::artist::{Line2D, Patch};
use crate::figure::Axes;
/// Fraction of the data span used as the longest arrow's length.
const ARROW_LENGTH_FRACTION: f64 = 0.15;
/// Arrowhead length as a fraction of an individual arrow's length.
const HEAD_LENGTH_FRACTION: f64 = 0.35;
/// Arrowhead half-width as a fraction of an individual arrow's length.
const HEAD_HALF_WIDTH_FRACTION: f64 = 0.2;
impl Axes {
/// Draw a 2D vector field: an arrow at each base `(x[i], y[i])` pointing
/// along `(u[i], v[i])`.
///
/// Mirrors matplotlib's `quiver`. The four slices give the arrow base
/// positions (`x`, `y`) and the vector components (`u`, `v`); they should
/// share a length `N`. Ragged inputs use the shortest length, and empty
/// input draws nothing (never panics).
///
/// Arrow lengths are auto-scaled deterministically so the longest vector
/// spans `0.15` of the larger base-point span (`max(xmax - xmin,
/// ymax - ymin)`, guarded to `1.0` when degenerate). With `mag[i] =
/// hypot(u[i], v[i])` and `max_mag = max(mag)`, the scale is `s =
/// L / max_mag` (or `0` when every vector is zero), giving each arrow a tip
/// `T = (x[i] + u[i]*s, y[i] + v[i]*s)`. A longer vector therefore always
/// produces a longer arrow.
///
/// Each arrow is a [`Line2D`] shaft from the base to `T - hl*dir` (so the
/// head is not doubled) plus a filled triangular [`Patch`] arrowhead at
/// `T`, where `dir = (u, v)/mag`, head length `hl = 0.35 * len_i` and
/// half-width `hw = 0.2 * len_i` for this arrow's length `len_i = mag[i]*s`.
/// A zero-magnitude vector contributes no shaft or head. All arrows share a
/// single color (the next property-cycle color), and the data limits expand
/// to cover every base and every tip. Returns `&mut Self`.
///
/// 
///
/// ```
/// use rizzma::core::Bbox;
/// use rizzma::figure::Axes;
///
/// let mut ax = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
/// // A single rightward unit vector at the origin.
/// ax.quiver(&[0.0], &[0.0], &[1.0], &[0.0]);
/// let limits = ax.data_limits().expect("quiver contributes data limits");
/// // Span is degenerate (one point), so it is guarded to 1.0 and the arrow
/// // reaches x = 0.15.
/// assert!((limits.xmax() - 0.15).abs() < 1e-9);
/// ```
pub fn quiver(&mut self, x: &[f64], y: &[f64], u: &[f64], v: &[f64]) -> &mut Self {
let n = x.len().min(y.len()).min(u.len()).min(v.len());
if n == 0 {
return self;
}
// Per-vector magnitudes and the largest, for deterministic auto-scale.
let mut max_mag = 0.0_f64;
let mut mag = Vec::with_capacity(n);
for i in 0..n {
let m = u[i].hypot(v[i]);
mag.push(m);
if m > max_mag {
max_mag = m;
}
}
// Base-point span; guard a zero/degenerate extent to 1.0.
let (mut xmin, mut xmax) = (x[0], x[0]);
let (mut ymin, mut ymax) = (y[0], y[0]);
for i in 0..n {
xmin = xmin.min(x[i]);
xmax = xmax.max(x[i]);
ymin = ymin.min(y[i]);
ymax = ymax.max(y[i]);
}
let span = (xmax - xmin).max(ymax - ymin);
let span = if span > 0.0 { span } else { 1.0 };
let target = ARROW_LENGTH_FRACTION * span;
let scale = if max_mag > 0.0 { target / max_mag } else { 0.0 };
let color = self.next_cycle_color();
for i in 0..n {
let base = [x[i], y[i]];
let tip = [x[i] + u[i] * scale, y[i] + v[i] * scale];
// A zero-magnitude vector has no direction: draw nothing for it.
if mag[i] == 0.0 || scale == 0.0 {
continue;
}
let len_i = mag[i] * scale;
let dir = [u[i] / mag[i], v[i] / mag[i]];
let perp = [-dir[1], dir[0]];
let hl = HEAD_LENGTH_FRACTION * len_i;
let hw = HEAD_HALF_WIDTH_FRACTION * len_i;
// Shaft ends where the arrowhead begins so the head is not doubled.
let neck = [tip[0] - hl * dir[0], tip[1] - hl * dir[1]];
self.add_line(
Line2D::new(vec![base[0], neck[0]], vec![base[1], neck[1]]).with_color(color),
);
// Filled triangle arrowhead at the tip, pointing along `dir`.
let left = [neck[0] + hw * perp[0], neck[1] + hw * perp[1]];
let right = [neck[0] - hw * perp[0], neck[1] - hw * perp[1]];
let head = Patch::polygon(&[tip, left, right])
.facecolor(Some(color))
.edgecolor(Some(color));
self.add_patch(head);
}
self
}
}
#[cfg(test)]
mod tests {
use crate::core::Bbox;
use crate::figure::Axes;
fn approx(a: f64, b: f64) {
assert!((a - b).abs() < 1e-9, "expected {b}, got {a}");
}
#[test]
fn quiver_adds_one_shaft_and_head_per_arrow() {
let mut ax = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
let x = [0.0, 1.0, 2.0];
let y = [0.0, 0.0, 0.0];
let u = [1.0, 0.0, 1.0];
let v = [0.0, 1.0, 1.0];
ax.quiver(&x, &y, &u, &v);
// One shaft (Line2D) and one head (Patch) per non-zero arrow.
assert_eq!(ax.lines.len(), 3);
assert_eq!(ax.patches.len(), 3);
}
#[test]
fn quiver_longer_vector_yields_longer_arrow() {
let mut ax = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
// Two arrows along +x from different bases so the span is non-zero;
// the second vector is longer.
let x = [0.0, 10.0];
let y = [0.0, 0.0];
let u = [1.0, 3.0];
let v = [0.0, 0.0];
ax.quiver(&x, &y, &u, &v);
// Each shaft is a horizontal segment; compare their lengths.
let len0 = {
let p = ax.lines[0].points();
(p[1][0] - p[0][0]).abs()
};
let len1 = {
let p = ax.lines[1].points();
(p[1][0] - p[0][0]).abs()
};
assert!(len1 > len0, "longer vector must give a longer shaft");
}
#[test]
fn quiver_zero_vector_draws_nothing() {
let mut ax = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
let x = [0.0, 1.0];
let y = [0.0, 0.0];
let u = [0.0, 1.0];
let v = [0.0, 0.0];
ax.quiver(&x, &y, &u, &v);
// Only the non-zero vector contributes a shaft and head.
assert_eq!(ax.lines.len(), 1);
assert_eq!(ax.patches.len(), 1);
}
#[test]
fn quiver_data_limits_cover_bases_and_tips() {
let mut ax = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
// Bases span x in [0, 1]; both vectors point in +x, so the rightmost
// tip (from the base at x = 1) pushes xmax past the base extent while
// the leftmost base at x = 0 fixes xmin.
let x = [0.0, 1.0];
let y = [0.0, 0.0];
let u = [1.0, 1.0];
let v = [0.0, 0.0];
ax.quiver(&x, &y, &u, &v);
let limits = ax.data_limits().expect("quiver contributes data limits");
// Leftmost base sits at x = 0.
approx(limits.xmin(), 0.0);
// span = 1.0 (x in [0, 1]); target = 0.15; max_mag = 1; tip at 1.15.
approx(limits.xmax(), 1.15);
}
#[test]
fn quiver_empty_input_draws_nothing() {
let mut ax = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
ax.quiver(&[], &[], &[], &[]);
assert!(ax.lines.is_empty());
assert!(ax.patches.is_empty());
assert!(ax.data_limits().is_none());
}
}