1use std::cell::{Cell, RefCell};
14use std::ops::{Deref, Range};
15use std::rc::Rc;
16
17use oxframe_2d::{
18 Accent, BaseProps, Circle, Custom, Group, IntoPaint, Line, Muted, Node, NodeBuilder, NodeId,
19 Path, Rect, Span, Text, Txt, pct,
20};
21use oxframe_core::kurbo::{BezPath, Point, Vec2};
22use oxframe_core::{Color, Lerp, Signal, Tween, all};
23
24#[derive(Debug, Clone, Copy, PartialEq)]
26pub struct Interval {
27 pub min: f64,
29 pub max: f64,
31}
32
33impl Interval {
34 pub fn len(&self) -> f64 {
36 let d = self.max - self.min;
37 if d.abs() < 1e-12 { 1e-12 } else { d }
38 }
39
40 pub fn is_empty(&self) -> bool {
42 false
43 }
44}
45
46impl From<Range<f64>> for Interval {
47 fn from(r: Range<f64>) -> Self {
48 Self {
49 min: r.start,
50 max: r.end,
51 }
52 }
53}
54
55impl From<(f64, f64)> for Interval {
56 fn from((min, max): (f64, f64)) -> Self {
57 Self { min, max }
58 }
59}
60
61impl Lerp for Interval {
62 fn lerp(&self, to: &Self, t: f64) -> Self {
63 Self {
64 min: self.min.lerp(&to.min, t),
65 max: self.max.lerp(&to.max, t),
66 }
67 }
68}
69
70const LABELS: usize = 24;
72const TARGET_TICKS: f64 = 8.0;
74
75fn tick_step(span: f64) -> f64 {
77 let raw = span.abs().max(1e-12) / TARGET_TICKS;
78 let mag = 10f64.powf(raw.log10().floor());
79 for k in [1.0, 2.0, 5.0, 10.0] {
80 if k * mag >= raw {
81 return k * mag;
82 }
83 }
84 10.0 * mag
85}
86
87fn tick_indices(range: Interval, step: f64) -> Range<i64> {
89 let (lo, hi) = (range.min.min(range.max), range.min.max(range.max));
90 let first = (lo / step - 1e-9).ceil() as i64;
91 let last = (hi / step + 1e-9).floor() as i64;
92 first..last + 1
93}
94
95fn label(value: f64, step: f64) -> String {
97 let decimals = if step >= 1.0 {
98 0
99 } else {
100 (-step.log10().floor()) as usize
101 };
102 let v = if value.abs() < step * 1e-6 {
103 0.0
104 } else {
105 value
106 };
107 format!("{v:.decimals$}")
108}
109
110struct Shared {
111 x: Signal<Interval>,
112 y: Signal<Interval>,
113 width: f64,
114 height: f64,
115 y_axis: bool,
117 categories: Cell<bool>,
120 matrix: Signal<Mat2>,
122}
123
124impl Shared {
125 fn c2p(&self, x: f64, y: f64) -> Vec2 {
127 let (x, y) = self.matrix.get().apply(x, y);
128 self.fixed(x, y)
129 }
130
131 fn fixed(&self, x: f64, y: f64) -> Vec2 {
133 map(self.x.get(), self.y.get(), self.width, self.height, x, y)
134 }
135}
136
137#[derive(Debug, Clone, Copy, PartialEq)]
140pub struct Mat2(pub [[f64; 2]; 2]);
141
142impl Mat2 {
143 pub const IDENTITY: Mat2 = Mat2([[1.0, 0.0], [0.0, 1.0]]);
145
146 pub fn apply(&self, x: f64, y: f64) -> (f64, f64) {
148 let [[a, b], [c, d]] = self.0;
149 (a * x + b * y, c * x + d * y)
150 }
151
152 pub fn after(&self, first: &Mat2) -> Mat2 {
154 let ([[a, b], [c, d]], [[e, f], [g, h]]) = (self.0, first.0);
155 Mat2([
156 [a * e + b * g, a * f + b * h],
157 [c * e + d * g, c * f + d * h],
158 ])
159 }
160
161 pub fn det(&self) -> f64 {
163 let [[a, b], [c, d]] = self.0;
164 a * d - b * c
165 }
166
167 fn inverse(&self) -> Option<Mat2> {
168 let det = self.det();
169 if det.abs() < 1e-9 {
170 return None;
171 }
172 let [[a, b], [c, d]] = self.0;
173 Some(Mat2([[d / det, -b / det], [-c / det, a / det]]))
174 }
175}
176
177impl Default for Mat2 {
178 fn default() -> Self {
179 Self::IDENTITY
180 }
181}
182
183impl From<[[f64; 2]; 2]> for Mat2 {
184 fn from(m: [[f64; 2]; 2]) -> Self {
185 Mat2(m)
186 }
187}
188
189impl Lerp for Mat2 {
190 fn lerp(&self, to: &Self, t: f64) -> Self {
191 let mut out = self.0;
192 for (r, row) in out.iter_mut().enumerate() {
193 for (c, v) in row.iter_mut().enumerate() {
194 *v = v.lerp(&to.0[r][c], t);
195 }
196 }
197 Mat2(out)
198 }
199}
200
201fn map(xr: Interval, yr: Interval, w: f64, h: f64, x: f64, y: f64) -> Vec2 {
202 Vec2::new(
203 ((x - xr.min) / xr.len() - 0.5) * w,
204 -((y - yr.min) / yr.len() - 0.5) * h,
205 )
206}
207
208#[derive(Clone)]
218pub struct Axes {
219 node: Node,
220 shared: Rc<Shared>,
221}
222
223impl Axes {
224 #[allow(clippy::new_ret_no_self)]
226 pub fn new() -> AxesBuilder {
227 AxesBuilder {
228 base: BaseProps::default(),
229 x: (-5.0..5.0).into(),
230 y: (-5.0..5.0).into(),
231 width: pct(80),
232 height: pct(60),
233 grid: true,
234 y_axis: true,
235 }
236 }
237
238 pub fn x_range(&self) -> Signal<Interval> {
240 self.shared.x.clone()
241 }
242
243 pub fn y_range(&self) -> Signal<Interval> {
245 self.shared.y.clone()
246 }
247
248 pub fn c2p(&self, x: f64, y: f64) -> Vec2 {
251 self.shared.c2p(x, y)
252 }
253
254 pub fn place(&self, node: impl Into<Node>, x: f64, y: f64) {
257 let node = node.into();
258 node.reparent(self.node);
259 let s = self.shared.clone();
260 node.x().bind(move || s.c2p(x, y).x);
261 let s = self.shared.clone();
262 node.y().bind(move || s.c2p(x, y).y);
263 }
264
265 #[track_caller]
269 pub fn plot(&self, f: impl Fn(f64) -> f64 + 'static) -> Plot {
270 oxframe_2d::add_site(|| self.plot_at(f))
271 }
272
273 fn plot_at(&self, f: impl Fn(f64) -> f64 + 'static) -> Plot {
274 let curve = Rc::new(Curve {
275 f: RefCell::new(Rc::new(f)),
276 from: RefCell::new(None),
277 mix: Signal::new(1.0),
278 });
279 let path = self.node.add(
280 Path::new()
281 .fill(Color::TRANSPARENT)
282 .stroke(Accent)
283 .line_width(4.0)
284 .name("plot"),
285 );
286 let s = self.shared.clone();
287 let c = curve.clone();
288 path.data().bind(move || sample(&s, &|x| c.eval(x)));
289 Plot {
290 path,
291 curve,
292 shared: self.shared.clone(),
293 }
294 }
295
296 #[track_caller]
298 pub fn point_on(&self, plot: &Plot, x: Signal<f64>) -> Circle {
299 oxframe_2d::add_site(|| self.point_on_at(plot, x))
300 }
301
302 fn point_on_at(&self, plot: &Plot, x: Signal<f64>) -> Circle {
303 let dot = self
304 .node
305 .add(Circle::new().size(22.0).fill(Text).name("point"));
306 let (s, c, xs) = (self.shared.clone(), plot.curve.clone(), x.clone());
307 dot.x().bind(move || {
308 let x = xs.get();
309 s.c2p(x, c.eval(x)).x
310 });
311 let (s, c) = (self.shared.clone(), plot.curve.clone());
312 dot.y().bind(move || {
313 let x = x.get();
314 s.c2p(x, c.eval(x)).y
315 });
316 dot
317 }
318
319 #[track_caller]
321 pub fn line_series(&self, points: impl IntoIterator<Item = (f64, f64)>) -> Series {
322 oxframe_2d::add_site(|| self.line_series_at(points))
323 }
324
325 fn line_series_at(&self, points: impl IntoIterator<Item = (f64, f64)>) -> Series {
326 let points: Vec<Signal<Vec2>> = points
327 .into_iter()
328 .map(|(x, y)| Signal::new(Vec2::new(x, y)))
329 .collect();
330 let path = self.node.add(
331 Path::new()
332 .fill(Color::TRANSPARENT)
333 .stroke(Accent)
334 .line_width(4.0)
335 .name("series"),
336 );
337 let (s, pts) = (self.shared.clone(), points.clone());
338 path.data().bind(move || {
339 let mut out = BezPath::new();
340 for (i, p) in pts.iter().enumerate() {
341 let v = p.get();
342 let q = s.c2p(v.x, v.y).to_point();
343 if i == 0 {
344 out.move_to(q);
345 } else {
346 out.line_to(q);
347 }
348 }
349 out
350 });
351 Series { path, points }
352 }
353
354 #[track_caller]
356 pub fn scatter(&self, points: impl IntoIterator<Item = (f64, f64)>) -> Vec<Circle> {
357 oxframe_2d::add_site(|| self.scatter_at(points))
358 }
359
360 fn scatter_at(&self, points: impl IntoIterator<Item = (f64, f64)>) -> Vec<Circle> {
361 points
362 .into_iter()
363 .map(|(x, y)| {
364 let dot = self
365 .node
366 .add(Circle::new().size(16.0).fill(Accent).name("dot"));
367 self.place(dot, x, y);
368 dot
369 })
370 .collect()
371 }
372
373 #[track_caller]
376 pub fn bars(&self, values: impl IntoIterator<Item = f64>) -> Bars {
377 oxframe_2d::add_site(|| self.bars_at(values))
378 }
379
380 fn bars_at(&self, values: impl IntoIterator<Item = f64>) -> Bars {
381 self.shared.categories.set(true);
382 let mut bars = Vec::new();
383 for (i, v) in values.into_iter().enumerate() {
384 let value = Signal::new(v).with_name(format!("bar{}.value", i + 1));
385 let x = i as f64;
386 let rect = self
387 .node
388 .add(Rect::new().fill(Accent).name(format!("bar{}", i + 1)));
389 let s = self.shared.clone();
390 rect.width().bind(move || {
391 let a = s.c2p(x - 0.35, 0.0).x;
392 let b = s.c2p(x + 0.35, 0.0).x;
393 (b - a).abs()
394 });
395 let (s, val2) = (self.shared.clone(), value.clone());
396 rect.height()
397 .bind(move || (s.c2p(x, val2.get()).y - s.c2p(x, 0.0).y).abs());
398 let s2 = self.shared.clone();
399 rect.x().bind(move || s2.c2p(x, 0.0).x);
400 let (s3, val3) = (self.shared.clone(), value.clone());
401 rect.y()
402 .bind(move || (s3.c2p(x, val3.get()).y + s3.c2p(x, 0.0).y) / 2.0);
403 let text = self
404 .node
405 .add(Txt::new().caption().name(format!("bar{}_label", i + 1)));
406 let val4 = value.clone();
407 text.text().bind(move || label(val4.get(), 0.1));
408 let (s4, val5) = (self.shared.clone(), value.clone());
409 text.x().bind(move || s4.c2p(x, 0.0).x);
410 let s5 = self.shared.clone();
411 text.y()
412 .bind(move || s5.c2p(x, val5.get().max(0.0)).y - 28.0);
413 bars.push((rect, value));
414 }
415 Bars {
416 bars,
417 axes: self.clone(),
418 }
419 }
420
421 pub fn matrix(&self) -> Signal<Mat2> {
423 self.shared.matrix.clone()
424 }
425
426 pub fn apply_matrix(&self, m: impl Into<Mat2>, duration: f64) -> Tween<Mat2> {
431 let target = m.into().after(&self.shared.matrix.get());
432 self.shared.matrix.to(target, duration)
433 }
434
435 #[track_caller]
439 pub fn vector(&self, x: f64, y: f64) -> Vector {
440 oxframe_2d::add_site(|| self.vector_at(x, y))
441 }
442
443 fn vector_at(&self, x: f64, y: f64) -> Vector {
444 let (xs, ys) = (
445 Signal::new(x).with_name("Vector.x"),
446 Signal::new(y).with_name("Vector.y"),
447 );
448 let line = self.node.add(
449 Line::new()
450 .points([(0.0, 0.0), (0.0, 0.0)])
451 .end_arrow(true)
452 .stroke(Accent)
453 .line_width(6.0)
454 .name("vector"),
455 );
456 let (s, tx, ty) = (self.shared.clone(), xs.clone(), ys.clone());
457 line.points()
458 .bind(move || vec![s.c2p(0.0, 0.0), s.c2p(tx.get(), ty.get())]);
459 Vector {
460 line,
461 x: xs,
462 y: ys,
463 axes: self.clone(),
464 }
465 }
466
467 pub fn node(&self) -> Node {
469 self.node
470 }
471}
472
473#[derive(Clone)]
476pub struct Vector {
477 line: Line,
478 x: Signal<f64>,
479 y: Signal<f64>,
480 axes: Axes,
481}
482
483impl Vector {
484 pub fn x(&self) -> Signal<f64> {
486 self.x.clone()
487 }
488
489 pub fn y(&self) -> Signal<f64> {
491 self.y.clone()
492 }
493
494 pub fn to(
496 &self,
497 x: f64,
498 y: f64,
499 duration: f64,
500 ) -> impl std::future::Future<Output = ()> + 'static {
501 let tweens = [self.x.to(x, duration), self.y.to(y, duration)];
502 async move {
503 all(tweens).await;
504 }
505 }
506
507 pub fn stroke(self, paint: impl IntoPaint) -> Self {
509 self.line.stroke().set(paint.into_paint());
510 self
511 }
512
513 pub fn label(&self, text: impl Into<String>) -> Txt {
515 let txt = self
516 .axes
517 .node
518 .add(Txt::new().text(text).body().name("vector_label"));
519 let place = {
520 let (s, x, y) = (self.axes.shared.clone(), self.x.clone(), self.y.clone());
521 move || {
522 let (o, tip) = (s.c2p(0.0, 0.0), s.c2p(x.get(), y.get()));
523 let d = tip - o;
524 let len = d.hypot();
525 let dir = if len > 1e-6 {
526 d / len
527 } else {
528 Vec2::new(1.0, 0.0)
529 };
530 let side = Vec2::new(dir.y, -dir.x);
534 let side = if side.x - side.y >= 0.0 { side } else { -side };
535 tip + dir * 22.0 + side * 22.0
536 }
537 };
538 let p = place.clone();
539 txt.x().bind(move || p().x);
540 txt.y().bind(move || place().y);
541 txt
542 }
543
544 pub fn line(&self) -> Line {
546 self.line
547 }
548}
549
550impl Deref for Vector {
551 type Target = Line;
552 fn deref(&self) -> &Line {
553 &self.line
554 }
555}
556
557impl Deref for Axes {
558 type Target = Node;
559 fn deref(&self) -> &Node {
560 &self.node
561 }
562}
563
564type Func = Rc<dyn Fn(f64) -> f64>;
566
567struct Curve {
568 f: RefCell<Func>,
569 from: RefCell<Option<Func>>,
571 mix: Signal<f64>,
572}
573
574impl Curve {
575 fn eval(&self, x: f64) -> f64 {
576 let y = (self.f.borrow())(x);
577 match &*self.from.borrow() {
578 Some(g) => g(x).lerp(&y, self.mix.get()),
579 None => y,
580 }
581 }
582}
583
584#[derive(Clone)]
587pub struct Plot {
588 path: Path,
589 curve: Rc<Curve>,
590 shared: Rc<Shared>,
591}
592
593impl Plot {
594 pub fn stroke(self, paint: impl IntoPaint) -> Self {
596 self.path.stroke().set(paint.into_paint());
597 self
598 }
599
600 pub fn width(self, px: f64) -> Self {
602 self.path.line_width().set(px);
603 self
604 }
605
606 pub fn to_fn(
609 &self,
610 g: impl Fn(f64) -> f64 + 'static,
611 seconds: f64,
612 ) -> impl std::future::Future<Output = ()> + 'static {
613 let curve = self.curve.clone();
614 let old = curve.f.replace(Rc::new(g));
615 *curve.from.borrow_mut() = Some(old);
616 curve.mix.set(0.0);
617 let tween = curve.mix.to(1.0, seconds);
618 async move {
619 tween.await;
620 *curve.from.borrow_mut() = None;
621 }
622 }
623
624 pub fn eval(&self, x: f64) -> f64 {
626 self.curve.eval(x)
627 }
628
629 pub fn point_at(&self, x: f64) -> Vec2 {
631 self.shared.c2p(x, self.curve.eval(x))
632 }
633}
634
635impl Deref for Plot {
636 type Target = Path;
637 fn deref(&self) -> &Path {
638 &self.path
639 }
640}
641
642pub struct Series {
644 path: Path,
645 points: Vec<Signal<Vec2>>,
646}
647
648impl Series {
649 pub fn point(&self, i: usize) -> Signal<Vec2> {
655 self.points[i].clone()
656 }
657}
658
659impl Deref for Series {
660 type Target = Path;
661 fn deref(&self) -> &Path {
662 &self.path
663 }
664}
665
666pub struct Bars {
668 bars: Vec<(Rect, Signal<f64>)>,
669 axes: Axes,
670}
671
672impl Bars {
673 #[track_caller]
675 pub fn labels(self, names: impl IntoIterator<Item = impl Into<String>>) -> Self {
676 oxframe_2d::add_site(|| self.labels_at(names))
677 }
678
679 fn labels_at(self, names: impl IntoIterator<Item = impl Into<String>>) -> Self {
680 for (i, name) in names.into_iter().enumerate().take(self.bars.len()) {
681 let txt = self.axes.node.add(
682 Txt::new()
683 .text(name)
684 .caption()
685 .name(format!("bar{}_name", i + 1)),
686 );
687 let s = self.axes.shared.clone();
688 let x = i as f64;
689 txt.x().bind(move || s.c2p(x, 0.0).x);
690 let s = self.axes.shared.clone();
691 txt.y().bind(move || axis_cross(&s).1 + 30.0);
692 }
693 self
694 }
695
696 pub fn value(&self, i: usize) -> Signal<f64> {
702 self.bars[i].1.clone()
703 }
704
705 pub fn bar(&self, i: usize) -> Rect {
711 self.bars[i].0
712 }
713
714 pub fn len(&self) -> usize {
716 self.bars.len()
717 }
718
719 pub fn is_empty(&self) -> bool {
721 self.bars.is_empty()
722 }
723}
724
725fn sample(s: &Shared, f: &dyn Fn(f64) -> f64) -> BezPath {
728 let (xr, yr) = (s.x.get(), s.y.get());
729 let (w, h) = (s.width, s.height);
730 let to_p = |x: f64, y: f64| map(xr, yr, w, h, x, y).to_point();
731 let n = 240usize;
732 let x_at = |i: usize| xr.min + (xr.max - xr.min) * i as f64 / n as f64;
733 let mut pts = Vec::with_capacity(n * 2);
735 let mut prev: Option<(f64, Point)> = None;
736 for i in 0..=n {
737 let x = x_at(i);
738 let p = to_p(x, f(x));
739 match prev {
740 Some(a) => refine(&to_p, f, a, (x, p), 0, h, &mut |q| pts.push(q)),
741 None => pts.push(p),
742 }
743 prev = Some((x, p));
744 }
745 clip(&pts, h / 2.0)
746}
747
748fn clip(pts: &[Point], half: f64) -> BezPath {
751 let inside = |p: Point| p.y.is_finite() && p.y.abs() <= half + 1e-9;
752 let edge = |inn: Point, out: Point| {
754 let bound = if out.y > 0.0 { half } else { -half };
755 let t = ((bound - inn.y) / (out.y - inn.y)).clamp(0.0, 1.0);
756 Point::new(inn.x + (out.x - inn.x) * t, bound)
757 };
758 let mut out = BezPath::new();
759 let mut pen = false;
760 let mut last: Option<Point> = None;
761 for &p in pts {
762 match (last, inside(p)) {
763 (Some(_), true) if pen => out.line_to(p),
764 (Some(a), true) if a.y.is_finite() && !inside(a) => {
765 out.move_to(edge(p, a));
766 out.line_to(p);
767 pen = true;
768 }
769 (_, true) => {
770 out.move_to(p);
771 pen = true;
772 }
773 (Some(a), false) if pen && p.y.is_finite() => {
774 out.line_to(edge(a, p));
775 pen = false;
776 }
777 _ => pen = false,
778 }
779 last = Some(p);
780 }
781 out
782}
783
784fn refine(
788 to_p: &dyn Fn(f64, f64) -> Point,
789 f: &dyn Fn(f64) -> f64,
790 a: (f64, Point),
791 b: (f64, Point),
792 depth: u32,
793 h: f64,
794 emit: &mut dyn FnMut(Point),
795) {
796 let jump = (b.1.y - a.1.y).abs();
797 if !(a.1.y.is_finite() && b.1.y.is_finite()) || jump > h * 0.05 {
798 if depth >= 6 {
799 if jump > h * 0.5 || !b.1.y.is_finite() {
800 emit(Point::new(b.1.x, f64::NAN));
801 }
802 emit(b.1);
803 return;
804 }
805 let mx = (a.0 + b.0) / 2.0;
806 let m = to_p(mx, f(mx));
807 refine(to_p, f, a, (mx, m), depth + 1, h, emit);
808 refine(to_p, f, (mx, m), b, depth + 1, h, emit);
809 return;
810 }
811 emit(b.1);
812}
813
814#[must_use = "a builder does nothing until it is passed to `add`"]
816pub struct AxesBuilder {
817 base: BaseProps,
818 x: Interval,
819 y: Interval,
820 width: Span,
821 height: Span,
822 grid: bool,
823 y_axis: bool,
824}
825
826impl AxesBuilder {
827 pub fn x_range(mut self, r: impl Into<Interval>) -> Self {
829 self.x = r.into();
830 self
831 }
832
833 pub fn y_range(mut self, r: impl Into<Interval>) -> Self {
835 self.y = r.into();
836 self
837 }
838
839 pub fn width(mut self, v: impl Into<Span>) -> Self {
841 self.width = v.into();
842 self
843 }
844
845 pub fn height(mut self, v: impl Into<Span>) -> Self {
847 self.height = v.into();
848 self
849 }
850
851 pub fn no_grid(mut self) -> Self {
853 self.grid = false;
854 self
855 }
856}
857
858fn resolve(span: Span, frame: f64) -> f64 {
859 match span {
860 Span::Px(v) => v,
861 Span::Pct(p) => frame * p / 100.0,
862 }
863}
864
865impl NodeBuilder for AxesBuilder {
866 type Handle = Axes;
867 fn base(&mut self) -> &mut BaseProps {
868 &mut self.base
869 }
870 fn build(self, g: &oxframe_2d::SceneGraph, parent: NodeId) -> Axes {
871 let (fw, fh) = g.frame_size();
872 let width = resolve(self.width, fw).max(1.0);
873 let height = if self.y_axis {
874 resolve(self.height, fh).max(1.0)
875 } else {
876 0.0
877 };
878 let base = self.base;
879 let mut group = Group::new().x(base.x).y(base.y).opacity(base.opacity);
880 if let Some(name) = base.name {
881 group = group.name(name);
882 }
883 let root = Node::from_id(parent).add(group);
884 let shared = Rc::new(Shared {
885 x: Signal::new(self.x).with_name("Axes.x_range"),
886 y: Signal::new(self.y).with_name("Axes.y_range"),
887 width,
888 height,
889 y_axis: self.y_axis,
890 categories: Cell::new(false),
891 matrix: Signal::new(Mat2::IDENTITY).with_name("Axes.matrix"),
892 });
893 let (grid, y_axis) = (self.grid, self.y_axis);
894 let s = shared.clone();
895 root.add(
896 Custom::new(move |ctx, _| draw_frame(ctx, &s, grid, y_axis))
897 .size(width + 60.0, height.max(1.0) + 60.0)
898 .name("frame"),
899 );
900 labels(&root, &shared, true);
901 if y_axis {
902 labels(&root, &shared, false);
903 }
904 Axes {
905 node: *root,
906 shared,
907 }
908 }
909}
910
911fn draw_frame(ctx: &mut oxframe_2d::DrawCtx, s: &Shared, grid: bool, y_axis: bool) {
913 let (xr, yr) = (s.x.get(), s.y.get());
914 let (w, h) = (s.width, s.height);
915 let (hw, hh) = (w / 2.0, h / 2.0);
916 let p = |x: f64, y: f64| map(xr, yr, w, h, x, y);
917 let (ax_x, ax_y) = axis_cross(s);
918 let x_ticks = !s.categories.get();
919 let xs = tick_step(xr.max - xr.min);
920 let ys = tick_step(yr.max - yr.min);
921 if grid && y_axis {
922 for k in tick_indices(xr, xs).filter(|_| x_ticks) {
923 let x = p(k as f64 * xs, 0.0).x;
924 ctx.line((x, -hh), (x, hh)).stroke(Muted, 1.0);
925 }
926 for k in tick_indices(yr, ys) {
927 let y = p(0.0, k as f64 * ys).y;
928 ctx.line((-hw, y), (hw, y)).stroke(Muted, 1.0);
929 }
930 transformed_grid(ctx, s, xs, ys);
931 }
932 ctx.line((-hw - 12.0, ax_y), (hw + 18.0, ax_y))
933 .stroke(Text, 3.0);
934 arrow(ctx, Point::new(hw + 30.0, ax_y), Vec2::new(1.0, 0.0));
935 for k in tick_indices(xr, xs).filter(|_| x_ticks) {
936 let x = p(k as f64 * xs, 0.0).x;
937 ctx.line((x, ax_y - 8.0), (x, ax_y + 8.0)).stroke(Text, 2.0);
938 }
939 if y_axis {
940 ctx.line((ax_x, hh + 12.0), (ax_x, -hh - 18.0))
941 .stroke(Text, 3.0);
942 arrow(ctx, Point::new(ax_x, -hh - 30.0), Vec2::new(0.0, -1.0));
943 for k in tick_indices(yr, ys) {
944 let y = p(0.0, k as f64 * ys).y;
945 ctx.line((ax_x - 8.0, y), (ax_x + 8.0, y)).stroke(Text, 2.0);
946 }
947 }
948}
949
950fn transformed_grid(ctx: &mut oxframe_2d::DrawCtx, s: &Shared, xs: f64, ys: f64) {
953 let m = s.matrix.get();
954 if m == Mat2::IDENTITY {
955 return;
956 }
957 let (xr, yr) = (s.x.get(), s.y.get());
958 let (hw, hh) = (s.width / 2.0, s.height / 2.0);
959 let corners = [
963 (xr.min, yr.min),
964 (xr.min, yr.max),
965 (xr.max, yr.min),
966 (xr.max, yr.max),
967 ];
968 let (mut x0, mut x1, mut y0, mut y1) = (xr.min, xr.max, yr.min, yr.max);
969 if let Some(inv) = m.inverse() {
970 for (x, y) in corners {
971 let (u, v) = inv.apply(x, y);
972 x0 = x0.min(u);
973 x1 = x1.max(u);
974 y0 = y0.min(v);
975 y1 = y1.max(v);
976 }
977 }
978 let (x0, x1) = (x0.max(xr.min - 40.0 * xs), x1.min(xr.max + 40.0 * xs));
980 let (y0, y1) = (y0.max(yr.min - 40.0 * ys), y1.min(yr.max + 40.0 * ys));
981 let paint = Accent.into_paint().with_alpha(0.55);
982 let mut segment = |a: (f64, f64), b: (f64, f64), width: f64| {
983 if let Some((p, q)) = clip_to_box(s.c2p(a.0, a.1), s.c2p(b.0, b.1), hw, hh) {
984 ctx.line(p, q).stroke(paint, width);
985 }
986 };
987 for k in (x0 / xs).ceil() as i64..=(x1 / xs).floor() as i64 {
988 let x = k as f64 * xs;
989 segment((x, y0), (x, y1), if k == 0 { 3.0 } else { 1.5 });
990 }
991 for k in (y0 / ys).ceil() as i64..=(y1 / ys).floor() as i64 {
992 let y = k as f64 * ys;
993 segment((x0, y), (x1, y), if k == 0 { 3.0 } else { 1.5 });
994 }
995}
996
997fn clip_to_box(a: Vec2, b: Vec2, hw: f64, hh: f64) -> Option<((f64, f64), (f64, f64))> {
999 let d = b - a;
1000 let (mut t0, mut t1) = (0.0f64, 1.0f64);
1001 for (p, q) in [
1002 (-d.x, a.x + hw),
1003 (d.x, hw - a.x),
1004 (-d.y, a.y + hh),
1005 (d.y, hh - a.y),
1006 ] {
1007 if p.abs() < 1e-12 {
1008 if q < 0.0 {
1009 return None;
1010 }
1011 continue;
1012 }
1013 let t = q / p;
1014 if p < 0.0 {
1015 t0 = t0.max(t);
1016 } else {
1017 t1 = t1.min(t);
1018 }
1019 if t0 > t1 {
1020 return None;
1021 }
1022 }
1023 let p = a + d * t0;
1024 let q = a + d * t1;
1025 Some(((p.x, p.y), (q.x, q.y)))
1026}
1027
1028fn arrow(ctx: &mut oxframe_2d::DrawCtx, tip: Point, dir: Vec2) {
1029 let side = Vec2::new(-dir.y, dir.x);
1030 let mut path = BezPath::new();
1031 path.move_to(tip);
1032 path.line_to(tip - dir * 16.0 + side * 8.0);
1033 path.line_to(tip - dir * 16.0 - side * 8.0);
1034 path.close_path();
1035 ctx.path(path).fill(Text);
1036}
1037
1038fn labels(root: &Group, s: &Rc<Shared>, horizontal: bool) {
1041 for j in 0..LABELS {
1042 let txt = if horizontal {
1043 Txt::new().caption().fill(Muted).name(format!("x_label{j}"))
1044 } else {
1045 Txt::new()
1047 .caption()
1048 .fill(Muted)
1049 .anchor((1.0, 0.0))
1050 .name(format!("y_label{j}"))
1051 };
1052 let txt = root.add(txt);
1053 let pick = {
1055 let s = s.clone();
1056 move || -> Option<(f64, f64)> {
1057 if horizontal && s.categories.get() {
1058 return None;
1059 }
1060 let r = if horizontal { s.x.get() } else { s.y.get() };
1061 let step = tick_step(r.max - r.min);
1062 let (cx, cy) = axis_cross_data(&s);
1063 let skip = if horizontal {
1065 s.y_axis.then_some(cx)
1066 } else {
1067 Some(cy)
1068 };
1069 tick_indices(r, step)
1070 .find(|k| k.rem_euclid(LABELS as i64) == j as i64)
1071 .map(|k| (k as f64 * step, step))
1072 .filter(|(v, step)| skip.is_none_or(|c| (v - c).abs() > step * 1e-6))
1073 }
1074 };
1075 let p = pick.clone();
1076 txt.text()
1077 .bind(move || p().map(|(v, st)| label(v, st)).unwrap_or_default());
1078 let p = pick.clone();
1079 txt.opacity()
1080 .bind(move || if p().is_some() { 1.0 } else { 0.0 });
1081 let (p, s2) = (pick.clone(), s.clone());
1082 txt.x().bind(move || match p() {
1083 Some((v, _)) if horizontal => s2.fixed(v, axis_cross(&s2).1).x,
1084 Some(_) => axis_cross(&s2).0 - 14.0,
1085 None => 0.0,
1086 });
1087 let (p, s2) = (pick, s.clone());
1088 txt.y().bind(move || match p() {
1089 Some(_) if horizontal => axis_cross(&s2).1 + 30.0,
1090 Some((v, _)) => s2.fixed(axis_cross_data(&s2).0, v).y,
1091 None => 0.0,
1092 });
1093 }
1094}
1095
1096fn axis_cross(s: &Shared) -> (f64, f64) {
1098 let (x, y) = axis_cross_data(s);
1099 let p = s.fixed(x, y);
1100 (p.x, if s.height > 0.0 { p.y } else { 0.0 })
1101}
1102
1103fn axis_cross_data(s: &Shared) -> (f64, f64) {
1105 let (xr, yr) = (s.x.get(), s.y.get());
1106 let x = if s.categories.get() {
1108 xr.min.min(xr.max)
1109 } else {
1110 0f64.clamp(xr.min.min(xr.max), xr.min.max(xr.max))
1111 };
1112 (x, 0f64.clamp(yr.min.min(yr.max), yr.min.max(yr.max)))
1113}
1114
1115pub struct NumberLine;
1117
1118impl NumberLine {
1119 #[allow(clippy::new_ret_no_self)]
1122 pub fn new(range: impl Into<Interval>) -> AxesBuilder {
1123 AxesBuilder {
1124 base: BaseProps::default(),
1125 x: range.into(),
1126 y: (0.0..1.0).into(),
1127 width: pct(80),
1128 height: Span::Px(0.0),
1129 grid: false,
1130 y_axis: false,
1131 }
1132 }
1133}
1134
1135into_node! {
1136 Axes => |c| c.node;
1137}
1138
1139#[cfg(test)]
1140mod tests {
1141 use super::*;
1142
1143 #[test]
1144 fn steps_are_round_and_give_few_ticks() {
1145 assert_eq!(tick_step(10.0), 2.0);
1146 assert_eq!(tick_step(1.0), 0.2);
1147 assert_eq!(tick_step(100.0), 20.0);
1148 let r: Interval = (-5.0..5.0).into();
1149 assert_eq!(tick_indices(r, 2.0), -2..3);
1150 }
1151
1152 use oxframe_2d::{SceneGraph, View};
1153 use oxframe_core::kurbo::PathEl;
1154
1155 fn axes(graph: &std::rc::Rc<SceneGraph>) -> Axes {
1156 graph.enter(|| {
1157 View::current().add(
1158 Axes::new()
1159 .x_range(-5.0..5.0)
1160 .y_range(-2.0..2.0)
1161 .width(800.0)
1162 .height(400.0),
1163 )
1164 })
1165 }
1166
1167 #[test]
1168 fn a_matrix_moves_vectors_and_placed_nodes_but_not_the_ticks() {
1169 let near = |a: Vec2, b: Vec2| assert!((a - b).hypot() < 1e-9, "{a:?} != {b:?}");
1170 let graph = SceneGraph::new(1920, 1080);
1171 let axes = axes(&graph);
1172 graph.enter(|| {
1173 let v = axes.vector(1.0, 1.0);
1175 let dot = View::current().add(Circle::new().size(10.0));
1176 axes.place(dot, 2.0, 0.0);
1177 near(v.points().get()[1], Vec2::new(80.0, -100.0));
1178 axes.matrix().set(Mat2([[1.0, 1.0], [0.0, 1.0]]));
1179 near(v.points().get()[1], Vec2::new(160.0, -100.0));
1181 assert!((dot.x().get() - 160.0).abs() < 1e-9);
1182 near(axes.c2p(1.0, 1.0), Vec2::new(160.0, -100.0));
1183 near(axes.shared.fixed(1.0, 1.0), Vec2::new(80.0, -100.0));
1184 v.x().set(-1.0);
1185 near(v.points().get()[1], Vec2::new(0.0, -100.0));
1186 });
1187 }
1188
1189 #[test]
1190 fn matrices_compose_and_animate_entry_by_entry() {
1191 let shear = Mat2([[1.0, 1.0], [0.0, 1.0]]);
1192 let rot = Mat2([[0.0, -1.0], [1.0, 0.0]]);
1193 assert_eq!(rot.after(&shear), Mat2([[0.0, -1.0], [1.0, 1.0]]));
1194 assert_eq!(rot.det(), 1.0);
1195 assert_eq!(
1196 Mat2::IDENTITY.lerp(&shear, 0.5),
1197 Mat2([[1.0, 0.5], [0.0, 1.0]])
1198 );
1199 let inv = shear.inverse().unwrap();
1200 assert_eq!(inv.after(&shear), Mat2::IDENTITY);
1201 assert!(Mat2([[1.0, 2.0], [2.0, 4.0]]).inverse().is_none());
1202 }
1203
1204 #[test]
1205 fn grid_lines_are_clipped_to_the_box() {
1206 let inside = clip_to_box(Vec2::new(-10.0, 0.0), Vec2::new(10.0, 0.0), 50.0, 50.0);
1207 assert_eq!(inside, Some(((-10.0, 0.0), (10.0, 0.0))));
1208 let across = clip_to_box(Vec2::new(-100.0, 0.0), Vec2::new(100.0, 0.0), 50.0, 50.0);
1209 assert_eq!(across, Some(((-50.0, 0.0), (50.0, 0.0))));
1210 assert_eq!(
1211 clip_to_box(Vec2::new(-100.0, 80.0), Vec2::new(100.0, 80.0), 50.0, 50.0),
1212 None
1213 );
1214 }
1215
1216 #[test]
1217 fn tan_has_no_bridge_across_its_gaps_and_stays_in_the_box() {
1218 let graph = SceneGraph::new(1920, 1080);
1219 let a = axes(&graph);
1220 let tan = graph.enter(|| a.plot(f64::tan));
1221 let data = graph.enter(|| tan.data().get());
1222 let mut last: Option<Point> = None;
1223 let mut moves = 0;
1224 for el in data.elements() {
1225 match *el {
1226 PathEl::MoveTo(p) => {
1227 moves += 1;
1228 last = Some(p);
1229 }
1230 PathEl::LineTo(p) => {
1231 let a = last.unwrap();
1232 assert!(p.y.abs() <= 200.0 + 1e-6, "{p:?} leaves the box");
1233 assert!(
1234 (p.y - a.y).abs() < 200.0,
1235 "a line jumps from {a:?} to {p:?}"
1236 );
1237 last = Some(p);
1238 }
1239 _ => {}
1240 }
1241 }
1242 assert_eq!(moves, 3);
1245 }
1246
1247 #[test]
1248 fn a_label_keeps_its_value_while_the_range_slides() {
1249 let graph = SceneGraph::new(1920, 1080);
1250 let a = axes(&graph);
1251 let read = |graph: &SceneGraph| -> Vec<(String, String)> {
1252 graph
1253 .inspect_nodes()
1254 .into_iter()
1255 .filter(|n| n.name.starts_with("x_label"))
1256 .map(|n| (n.name, n.text.unwrap_or_default()))
1257 .collect()
1258 };
1259 let before = read(&graph);
1260 graph.enter(|| a.x_range().set(-4.8..5.2));
1261 let after = read(&graph);
1262 for ((name, v), (_, w)) in before.iter().zip(&after) {
1263 if !v.is_empty() && !w.is_empty() {
1264 assert_eq!(v, w, "{name} changed its value while sliding");
1265 }
1266 }
1267 }
1268
1269 #[test]
1270 fn a_range_change_moves_the_curve() {
1271 let graph = SceneGraph::new(1920, 1080);
1272 let a = axes(&graph);
1273 let sin = graph.enter(|| a.plot(f64::sin));
1274 let first = graph.enter(|| sin.data().get());
1275 graph.enter(|| a.x_range().set(0.0..10.0));
1276 assert_ne!(first, graph.enter(|| sin.data().get()));
1277 let p = graph.enter(|| a.c2p(5.0, 0.0));
1278 assert!(p.x.abs() < 1e-9 && p.y.abs() < 1e-9, "{p:?}");
1279 }
1280
1281 #[test]
1282 fn plots_report_the_line_that_added_them() {
1283 let graph = SceneGraph::new(1920, 1080);
1284 let a = axes(&graph);
1285 graph.enter(|| {
1286 let _ = a.plot(f64::sin);
1287 });
1288 let line = line!() - 2;
1289 let plot = graph
1290 .inspect_nodes()
1291 .into_iter()
1292 .find(|n| n.kind == "Path")
1293 .unwrap();
1294 assert_eq!(
1295 plot.source.as_deref(),
1296 Some(format!("src/axes.rs:{line}").as_str())
1297 );
1298 }
1299
1300 #[test]
1301 fn labels_have_the_decimals_the_step_needs() {
1302 assert_eq!(label(0.4, 0.2), "0.4");
1303 assert_eq!(label(3.0, 1.0), "3");
1304 assert_eq!(label(-1e-12, 0.5), "0.0");
1305 }
1306}