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 tip + dir * 30.0
531 }
532 };
533 let p = place.clone();
534 txt.x().bind(move || p().x);
535 txt.y().bind(move || place().y);
536 txt
537 }
538
539 pub fn line(&self) -> Line {
541 self.line
542 }
543}
544
545impl Deref for Vector {
546 type Target = Line;
547 fn deref(&self) -> &Line {
548 &self.line
549 }
550}
551
552impl Deref for Axes {
553 type Target = Node;
554 fn deref(&self) -> &Node {
555 &self.node
556 }
557}
558
559type Func = Rc<dyn Fn(f64) -> f64>;
561
562struct Curve {
563 f: RefCell<Func>,
564 from: RefCell<Option<Func>>,
566 mix: Signal<f64>,
567}
568
569impl Curve {
570 fn eval(&self, x: f64) -> f64 {
571 let y = (self.f.borrow())(x);
572 match &*self.from.borrow() {
573 Some(g) => g(x).lerp(&y, self.mix.get()),
574 None => y,
575 }
576 }
577}
578
579#[derive(Clone)]
582pub struct Plot {
583 path: Path,
584 curve: Rc<Curve>,
585 shared: Rc<Shared>,
586}
587
588impl Plot {
589 pub fn stroke(self, paint: impl IntoPaint) -> Self {
591 self.path.stroke().set(paint.into_paint());
592 self
593 }
594
595 pub fn width(self, px: f64) -> Self {
597 self.path.line_width().set(px);
598 self
599 }
600
601 pub fn to_fn(
604 &self,
605 g: impl Fn(f64) -> f64 + 'static,
606 seconds: f64,
607 ) -> impl std::future::Future<Output = ()> + 'static {
608 let curve = self.curve.clone();
609 let old = curve.f.replace(Rc::new(g));
610 *curve.from.borrow_mut() = Some(old);
611 curve.mix.set(0.0);
612 let tween = curve.mix.to(1.0, seconds);
613 async move {
614 tween.await;
615 *curve.from.borrow_mut() = None;
616 }
617 }
618
619 pub fn eval(&self, x: f64) -> f64 {
621 self.curve.eval(x)
622 }
623
624 pub fn point_at(&self, x: f64) -> Vec2 {
626 self.shared.c2p(x, self.curve.eval(x))
627 }
628}
629
630impl Deref for Plot {
631 type Target = Path;
632 fn deref(&self) -> &Path {
633 &self.path
634 }
635}
636
637pub struct Series {
639 path: Path,
640 points: Vec<Signal<Vec2>>,
641}
642
643impl Series {
644 pub fn point(&self, i: usize) -> Signal<Vec2> {
650 self.points[i].clone()
651 }
652}
653
654impl Deref for Series {
655 type Target = Path;
656 fn deref(&self) -> &Path {
657 &self.path
658 }
659}
660
661pub struct Bars {
663 bars: Vec<(Rect, Signal<f64>)>,
664 axes: Axes,
665}
666
667impl Bars {
668 #[track_caller]
670 pub fn labels(self, names: impl IntoIterator<Item = impl Into<String>>) -> Self {
671 oxframe_2d::add_site(|| self.labels_at(names))
672 }
673
674 fn labels_at(self, names: impl IntoIterator<Item = impl Into<String>>) -> Self {
675 for (i, name) in names.into_iter().enumerate().take(self.bars.len()) {
676 let txt = self.axes.node.add(
677 Txt::new()
678 .text(name)
679 .caption()
680 .name(format!("bar{}_name", i + 1)),
681 );
682 let s = self.axes.shared.clone();
683 let x = i as f64;
684 txt.x().bind(move || s.c2p(x, 0.0).x);
685 let s = self.axes.shared.clone();
686 txt.y().bind(move || axis_cross(&s).1 + 30.0);
687 }
688 self
689 }
690
691 pub fn value(&self, i: usize) -> Signal<f64> {
697 self.bars[i].1.clone()
698 }
699
700 pub fn bar(&self, i: usize) -> Rect {
706 self.bars[i].0
707 }
708
709 pub fn len(&self) -> usize {
711 self.bars.len()
712 }
713
714 pub fn is_empty(&self) -> bool {
716 self.bars.is_empty()
717 }
718}
719
720fn sample(s: &Shared, f: &dyn Fn(f64) -> f64) -> BezPath {
723 let (xr, yr) = (s.x.get(), s.y.get());
724 let (w, h) = (s.width, s.height);
725 let to_p = |x: f64, y: f64| map(xr, yr, w, h, x, y).to_point();
726 let n = 240usize;
727 let x_at = |i: usize| xr.min + (xr.max - xr.min) * i as f64 / n as f64;
728 let mut pts = Vec::with_capacity(n * 2);
730 let mut prev: Option<(f64, Point)> = None;
731 for i in 0..=n {
732 let x = x_at(i);
733 let p = to_p(x, f(x));
734 match prev {
735 Some(a) => refine(&to_p, f, a, (x, p), 0, h, &mut |q| pts.push(q)),
736 None => pts.push(p),
737 }
738 prev = Some((x, p));
739 }
740 clip(&pts, h / 2.0)
741}
742
743fn clip(pts: &[Point], half: f64) -> BezPath {
746 let inside = |p: Point| p.y.is_finite() && p.y.abs() <= half + 1e-9;
747 let edge = |inn: Point, out: Point| {
749 let bound = if out.y > 0.0 { half } else { -half };
750 let t = ((bound - inn.y) / (out.y - inn.y)).clamp(0.0, 1.0);
751 Point::new(inn.x + (out.x - inn.x) * t, bound)
752 };
753 let mut out = BezPath::new();
754 let mut pen = false;
755 let mut last: Option<Point> = None;
756 for &p in pts {
757 match (last, inside(p)) {
758 (Some(_), true) if pen => out.line_to(p),
759 (Some(a), true) if a.y.is_finite() && !inside(a) => {
760 out.move_to(edge(p, a));
761 out.line_to(p);
762 pen = true;
763 }
764 (_, true) => {
765 out.move_to(p);
766 pen = true;
767 }
768 (Some(a), false) if pen && p.y.is_finite() => {
769 out.line_to(edge(a, p));
770 pen = false;
771 }
772 _ => pen = false,
773 }
774 last = Some(p);
775 }
776 out
777}
778
779fn refine(
783 to_p: &dyn Fn(f64, f64) -> Point,
784 f: &dyn Fn(f64) -> f64,
785 a: (f64, Point),
786 b: (f64, Point),
787 depth: u32,
788 h: f64,
789 emit: &mut dyn FnMut(Point),
790) {
791 let jump = (b.1.y - a.1.y).abs();
792 if !(a.1.y.is_finite() && b.1.y.is_finite()) || jump > h * 0.05 {
793 if depth >= 6 {
794 if jump > h * 0.5 || !b.1.y.is_finite() {
795 emit(Point::new(b.1.x, f64::NAN));
796 }
797 emit(b.1);
798 return;
799 }
800 let mx = (a.0 + b.0) / 2.0;
801 let m = to_p(mx, f(mx));
802 refine(to_p, f, a, (mx, m), depth + 1, h, emit);
803 refine(to_p, f, (mx, m), b, depth + 1, h, emit);
804 return;
805 }
806 emit(b.1);
807}
808
809#[must_use = "a builder does nothing until it is passed to `add`"]
811pub struct AxesBuilder {
812 base: BaseProps,
813 x: Interval,
814 y: Interval,
815 width: Span,
816 height: Span,
817 grid: bool,
818 y_axis: bool,
819}
820
821impl AxesBuilder {
822 pub fn x_range(mut self, r: impl Into<Interval>) -> Self {
824 self.x = r.into();
825 self
826 }
827
828 pub fn y_range(mut self, r: impl Into<Interval>) -> Self {
830 self.y = r.into();
831 self
832 }
833
834 pub fn width(mut self, v: impl Into<Span>) -> Self {
836 self.width = v.into();
837 self
838 }
839
840 pub fn height(mut self, v: impl Into<Span>) -> Self {
842 self.height = v.into();
843 self
844 }
845
846 pub fn no_grid(mut self) -> Self {
848 self.grid = false;
849 self
850 }
851}
852
853fn resolve(span: Span, frame: f64) -> f64 {
854 match span {
855 Span::Px(v) => v,
856 Span::Pct(p) => frame * p / 100.0,
857 }
858}
859
860impl NodeBuilder for AxesBuilder {
861 type Handle = Axes;
862 fn base(&mut self) -> &mut BaseProps {
863 &mut self.base
864 }
865 fn build(self, g: &oxframe_2d::SceneGraph, parent: NodeId) -> Axes {
866 let (fw, fh) = g.frame_size();
867 let width = resolve(self.width, fw).max(1.0);
868 let height = if self.y_axis {
869 resolve(self.height, fh).max(1.0)
870 } else {
871 0.0
872 };
873 let base = self.base;
874 let mut group = Group::new().x(base.x).y(base.y).opacity(base.opacity);
875 if let Some(name) = base.name {
876 group = group.name(name);
877 }
878 let root = Node::from_id(parent).add(group);
879 let shared = Rc::new(Shared {
880 x: Signal::new(self.x).with_name("Axes.x_range"),
881 y: Signal::new(self.y).with_name("Axes.y_range"),
882 width,
883 height,
884 y_axis: self.y_axis,
885 categories: Cell::new(false),
886 matrix: Signal::new(Mat2::IDENTITY).with_name("Axes.matrix"),
887 });
888 let (grid, y_axis) = (self.grid, self.y_axis);
889 let s = shared.clone();
890 root.add(
891 Custom::new(move |ctx, _| draw_frame(ctx, &s, grid, y_axis))
892 .size(width + 60.0, height.max(1.0) + 60.0)
893 .name("frame"),
894 );
895 labels(&root, &shared, true);
896 if y_axis {
897 labels(&root, &shared, false);
898 }
899 Axes {
900 node: *root,
901 shared,
902 }
903 }
904}
905
906fn draw_frame(ctx: &mut oxframe_2d::DrawCtx, s: &Shared, grid: bool, y_axis: bool) {
908 let (xr, yr) = (s.x.get(), s.y.get());
909 let (w, h) = (s.width, s.height);
910 let (hw, hh) = (w / 2.0, h / 2.0);
911 let p = |x: f64, y: f64| map(xr, yr, w, h, x, y);
912 let (ax_x, ax_y) = axis_cross(s);
913 let x_ticks = !s.categories.get();
914 let xs = tick_step(xr.max - xr.min);
915 let ys = tick_step(yr.max - yr.min);
916 if grid && y_axis {
917 for k in tick_indices(xr, xs).filter(|_| x_ticks) {
918 let x = p(k as f64 * xs, 0.0).x;
919 ctx.line((x, -hh), (x, hh)).stroke(Muted, 1.0);
920 }
921 for k in tick_indices(yr, ys) {
922 let y = p(0.0, k as f64 * ys).y;
923 ctx.line((-hw, y), (hw, y)).stroke(Muted, 1.0);
924 }
925 transformed_grid(ctx, s, xs, ys);
926 }
927 ctx.line((-hw - 12.0, ax_y), (hw + 18.0, ax_y))
928 .stroke(Text, 3.0);
929 arrow(ctx, Point::new(hw + 30.0, ax_y), Vec2::new(1.0, 0.0));
930 for k in tick_indices(xr, xs).filter(|_| x_ticks) {
931 let x = p(k as f64 * xs, 0.0).x;
932 ctx.line((x, ax_y - 8.0), (x, ax_y + 8.0)).stroke(Text, 2.0);
933 }
934 if y_axis {
935 ctx.line((ax_x, hh + 12.0), (ax_x, -hh - 18.0))
936 .stroke(Text, 3.0);
937 arrow(ctx, Point::new(ax_x, -hh - 30.0), Vec2::new(0.0, -1.0));
938 for k in tick_indices(yr, ys) {
939 let y = p(0.0, k as f64 * ys).y;
940 ctx.line((ax_x - 8.0, y), (ax_x + 8.0, y)).stroke(Text, 2.0);
941 }
942 }
943}
944
945fn transformed_grid(ctx: &mut oxframe_2d::DrawCtx, s: &Shared, xs: f64, ys: f64) {
948 let m = s.matrix.get();
949 if m == Mat2::IDENTITY {
950 return;
951 }
952 let (xr, yr) = (s.x.get(), s.y.get());
953 let (hw, hh) = (s.width / 2.0, s.height / 2.0);
954 let corners = [
958 (xr.min, yr.min),
959 (xr.min, yr.max),
960 (xr.max, yr.min),
961 (xr.max, yr.max),
962 ];
963 let (mut x0, mut x1, mut y0, mut y1) = (xr.min, xr.max, yr.min, yr.max);
964 if let Some(inv) = m.inverse() {
965 for (x, y) in corners {
966 let (u, v) = inv.apply(x, y);
967 x0 = x0.min(u);
968 x1 = x1.max(u);
969 y0 = y0.min(v);
970 y1 = y1.max(v);
971 }
972 }
973 let (x0, x1) = (x0.max(xr.min - 40.0 * xs), x1.min(xr.max + 40.0 * xs));
975 let (y0, y1) = (y0.max(yr.min - 40.0 * ys), y1.min(yr.max + 40.0 * ys));
976 let paint = Accent.into_paint().with_alpha(0.55);
977 let mut segment = |a: (f64, f64), b: (f64, f64), width: f64| {
978 if let Some((p, q)) = clip_to_box(s.c2p(a.0, a.1), s.c2p(b.0, b.1), hw, hh) {
979 ctx.line(p, q).stroke(paint, width);
980 }
981 };
982 for k in (x0 / xs).ceil() as i64..=(x1 / xs).floor() as i64 {
983 let x = k as f64 * xs;
984 segment((x, y0), (x, y1), if k == 0 { 3.0 } else { 1.5 });
985 }
986 for k in (y0 / ys).ceil() as i64..=(y1 / ys).floor() as i64 {
987 let y = k as f64 * ys;
988 segment((x0, y), (x1, y), if k == 0 { 3.0 } else { 1.5 });
989 }
990}
991
992fn clip_to_box(a: Vec2, b: Vec2, hw: f64, hh: f64) -> Option<((f64, f64), (f64, f64))> {
994 let d = b - a;
995 let (mut t0, mut t1) = (0.0f64, 1.0f64);
996 for (p, q) in [
997 (-d.x, a.x + hw),
998 (d.x, hw - a.x),
999 (-d.y, a.y + hh),
1000 (d.y, hh - a.y),
1001 ] {
1002 if p.abs() < 1e-12 {
1003 if q < 0.0 {
1004 return None;
1005 }
1006 continue;
1007 }
1008 let t = q / p;
1009 if p < 0.0 {
1010 t0 = t0.max(t);
1011 } else {
1012 t1 = t1.min(t);
1013 }
1014 if t0 > t1 {
1015 return None;
1016 }
1017 }
1018 let p = a + d * t0;
1019 let q = a + d * t1;
1020 Some(((p.x, p.y), (q.x, q.y)))
1021}
1022
1023fn arrow(ctx: &mut oxframe_2d::DrawCtx, tip: Point, dir: Vec2) {
1024 let side = Vec2::new(-dir.y, dir.x);
1025 let mut path = BezPath::new();
1026 path.move_to(tip);
1027 path.line_to(tip - dir * 16.0 + side * 8.0);
1028 path.line_to(tip - dir * 16.0 - side * 8.0);
1029 path.close_path();
1030 ctx.path(path).fill(Text);
1031}
1032
1033fn labels(root: &Group, s: &Rc<Shared>, horizontal: bool) {
1036 for j in 0..LABELS {
1037 let txt = if horizontal {
1038 Txt::new().caption().fill(Muted).name(format!("x_label{j}"))
1039 } else {
1040 Txt::new()
1042 .caption()
1043 .fill(Muted)
1044 .anchor((1.0, 0.0))
1045 .name(format!("y_label{j}"))
1046 };
1047 let txt = root.add(txt);
1048 let pick = {
1050 let s = s.clone();
1051 move || -> Option<(f64, f64)> {
1052 if horizontal && s.categories.get() {
1053 return None;
1054 }
1055 let r = if horizontal { s.x.get() } else { s.y.get() };
1056 let step = tick_step(r.max - r.min);
1057 let (cx, cy) = axis_cross_data(&s);
1058 let skip = if horizontal {
1060 s.y_axis.then_some(cx)
1061 } else {
1062 Some(cy)
1063 };
1064 tick_indices(r, step)
1065 .find(|k| k.rem_euclid(LABELS as i64) == j as i64)
1066 .map(|k| (k as f64 * step, step))
1067 .filter(|(v, step)| skip.is_none_or(|c| (v - c).abs() > step * 1e-6))
1068 }
1069 };
1070 let p = pick.clone();
1071 txt.text()
1072 .bind(move || p().map(|(v, st)| label(v, st)).unwrap_or_default());
1073 let p = pick.clone();
1074 txt.opacity()
1075 .bind(move || if p().is_some() { 1.0 } else { 0.0 });
1076 let (p, s2) = (pick.clone(), s.clone());
1077 txt.x().bind(move || match p() {
1078 Some((v, _)) if horizontal => s2.fixed(v, axis_cross(&s2).1).x,
1079 Some(_) => axis_cross(&s2).0 - 14.0,
1080 None => 0.0,
1081 });
1082 let (p, s2) = (pick, s.clone());
1083 txt.y().bind(move || match p() {
1084 Some(_) if horizontal => axis_cross(&s2).1 + 30.0,
1085 Some((v, _)) => s2.fixed(axis_cross_data(&s2).0, v).y,
1086 None => 0.0,
1087 });
1088 }
1089}
1090
1091fn axis_cross(s: &Shared) -> (f64, f64) {
1093 let (x, y) = axis_cross_data(s);
1094 let p = s.fixed(x, y);
1095 (p.x, if s.height > 0.0 { p.y } else { 0.0 })
1096}
1097
1098fn axis_cross_data(s: &Shared) -> (f64, f64) {
1100 let (xr, yr) = (s.x.get(), s.y.get());
1101 let x = if s.categories.get() {
1103 xr.min.min(xr.max)
1104 } else {
1105 0f64.clamp(xr.min.min(xr.max), xr.min.max(xr.max))
1106 };
1107 (x, 0f64.clamp(yr.min.min(yr.max), yr.min.max(yr.max)))
1108}
1109
1110pub struct NumberLine;
1112
1113impl NumberLine {
1114 #[allow(clippy::new_ret_no_self)]
1117 pub fn new(range: impl Into<Interval>) -> AxesBuilder {
1118 AxesBuilder {
1119 base: BaseProps::default(),
1120 x: range.into(),
1121 y: (0.0..1.0).into(),
1122 width: pct(80),
1123 height: Span::Px(0.0),
1124 grid: false,
1125 y_axis: false,
1126 }
1127 }
1128}
1129
1130#[cfg(test)]
1131mod tests {
1132 use super::*;
1133
1134 #[test]
1135 fn steps_are_round_and_give_few_ticks() {
1136 assert_eq!(tick_step(10.0), 2.0);
1137 assert_eq!(tick_step(1.0), 0.2);
1138 assert_eq!(tick_step(100.0), 20.0);
1139 let r: Interval = (-5.0..5.0).into();
1140 assert_eq!(tick_indices(r, 2.0), -2..3);
1141 }
1142
1143 use oxframe_2d::{SceneGraph, View};
1144 use oxframe_core::kurbo::PathEl;
1145
1146 fn axes(graph: &std::rc::Rc<SceneGraph>) -> Axes {
1147 graph.enter(|| {
1148 View::current().add(
1149 Axes::new()
1150 .x_range(-5.0..5.0)
1151 .y_range(-2.0..2.0)
1152 .width(800.0)
1153 .height(400.0),
1154 )
1155 })
1156 }
1157
1158 #[test]
1159 fn a_matrix_moves_vectors_and_placed_nodes_but_not_the_ticks() {
1160 let near = |a: Vec2, b: Vec2| assert!((a - b).hypot() < 1e-9, "{a:?} != {b:?}");
1161 let graph = SceneGraph::new(1920, 1080);
1162 let axes = axes(&graph);
1163 graph.enter(|| {
1164 let v = axes.vector(1.0, 1.0);
1166 let dot = View::current().add(Circle::new().size(10.0));
1167 axes.place(dot, 2.0, 0.0);
1168 near(v.points().get()[1], Vec2::new(80.0, -100.0));
1169 axes.matrix().set(Mat2([[1.0, 1.0], [0.0, 1.0]]));
1170 near(v.points().get()[1], Vec2::new(160.0, -100.0));
1172 assert!((dot.x().get() - 160.0).abs() < 1e-9);
1173 near(axes.c2p(1.0, 1.0), Vec2::new(160.0, -100.0));
1174 near(axes.shared.fixed(1.0, 1.0), Vec2::new(80.0, -100.0));
1175 v.x().set(-1.0);
1176 near(v.points().get()[1], Vec2::new(0.0, -100.0));
1177 });
1178 }
1179
1180 #[test]
1181 fn matrices_compose_and_animate_entry_by_entry() {
1182 let shear = Mat2([[1.0, 1.0], [0.0, 1.0]]);
1183 let rot = Mat2([[0.0, -1.0], [1.0, 0.0]]);
1184 assert_eq!(rot.after(&shear), Mat2([[0.0, -1.0], [1.0, 1.0]]));
1185 assert_eq!(rot.det(), 1.0);
1186 assert_eq!(
1187 Mat2::IDENTITY.lerp(&shear, 0.5),
1188 Mat2([[1.0, 0.5], [0.0, 1.0]])
1189 );
1190 let inv = shear.inverse().unwrap();
1191 assert_eq!(inv.after(&shear), Mat2::IDENTITY);
1192 assert!(Mat2([[1.0, 2.0], [2.0, 4.0]]).inverse().is_none());
1193 }
1194
1195 #[test]
1196 fn grid_lines_are_clipped_to_the_box() {
1197 let inside = clip_to_box(Vec2::new(-10.0, 0.0), Vec2::new(10.0, 0.0), 50.0, 50.0);
1198 assert_eq!(inside, Some(((-10.0, 0.0), (10.0, 0.0))));
1199 let across = clip_to_box(Vec2::new(-100.0, 0.0), Vec2::new(100.0, 0.0), 50.0, 50.0);
1200 assert_eq!(across, Some(((-50.0, 0.0), (50.0, 0.0))));
1201 assert_eq!(
1202 clip_to_box(Vec2::new(-100.0, 80.0), Vec2::new(100.0, 80.0), 50.0, 50.0),
1203 None
1204 );
1205 }
1206
1207 #[test]
1208 fn tan_has_no_bridge_across_its_gaps_and_stays_in_the_box() {
1209 let graph = SceneGraph::new(1920, 1080);
1210 let a = axes(&graph);
1211 let tan = graph.enter(|| a.plot(f64::tan));
1212 let data = graph.enter(|| tan.data().get());
1213 let mut last: Option<Point> = None;
1214 let mut moves = 0;
1215 for el in data.elements() {
1216 match *el {
1217 PathEl::MoveTo(p) => {
1218 moves += 1;
1219 last = Some(p);
1220 }
1221 PathEl::LineTo(p) => {
1222 let a = last.unwrap();
1223 assert!(p.y.abs() <= 200.0 + 1e-6, "{p:?} leaves the box");
1224 assert!(
1225 (p.y - a.y).abs() < 200.0,
1226 "a line jumps from {a:?} to {p:?}"
1227 );
1228 last = Some(p);
1229 }
1230 _ => {}
1231 }
1232 }
1233 assert_eq!(moves, 3);
1236 }
1237
1238 #[test]
1239 fn a_label_keeps_its_value_while_the_range_slides() {
1240 let graph = SceneGraph::new(1920, 1080);
1241 let a = axes(&graph);
1242 let read = |graph: &SceneGraph| -> Vec<(String, String)> {
1243 graph
1244 .inspect_nodes()
1245 .into_iter()
1246 .filter(|n| n.name.starts_with("x_label"))
1247 .map(|n| (n.name, n.text.unwrap_or_default()))
1248 .collect()
1249 };
1250 let before = read(&graph);
1251 graph.enter(|| a.x_range().set((-4.8..5.2).into()));
1252 let after = read(&graph);
1253 for ((name, v), (_, w)) in before.iter().zip(&after) {
1254 if !v.is_empty() && !w.is_empty() {
1255 assert_eq!(v, w, "{name} changed its value while sliding");
1256 }
1257 }
1258 }
1259
1260 #[test]
1261 fn a_range_change_moves_the_curve() {
1262 let graph = SceneGraph::new(1920, 1080);
1263 let a = axes(&graph);
1264 let sin = graph.enter(|| a.plot(f64::sin));
1265 let first = graph.enter(|| sin.data().get());
1266 graph.enter(|| a.x_range().set((0.0..10.0).into()));
1267 assert_ne!(first, graph.enter(|| sin.data().get()));
1268 let p = graph.enter(|| a.c2p(5.0, 0.0));
1269 assert!(p.x.abs() < 1e-9 && p.y.abs() < 1e-9, "{p:?}");
1270 }
1271
1272 #[test]
1273 fn plots_report_the_line_that_added_them() {
1274 let graph = SceneGraph::new(1920, 1080);
1275 let a = axes(&graph);
1276 graph.enter(|| {
1277 let _ = a.plot(f64::sin);
1278 });
1279 let line = line!() - 2;
1280 let plot = graph
1281 .inspect_nodes()
1282 .into_iter()
1283 .find(|n| n.kind == "Path")
1284 .unwrap();
1285 assert_eq!(
1286 plot.source.as_deref(),
1287 Some(format!("src/axes.rs:{line}").as_str())
1288 );
1289 }
1290
1291 #[test]
1292 fn labels_have_the_decimals_the_step_needs() {
1293 assert_eq!(label(0.4, 0.2), "0.4");
1294 assert_eq!(label(3.0, 1.0), "3");
1295 assert_eq!(label(-1e-12, 0.5), "0.0");
1296 }
1297}