diff --git a/TASKS.md b/TASKS.md index 0a29274..923bed9 100644 --- a/TASKS.md +++ b/TASKS.md @@ -55,11 +55,11 @@ Per-component lines live in `tasks/`. Tags there: - [x] T15a Charts · Cartesian - [x] T15b Charts · Parts and spreads - [x] T15c Charts · Flows and relations -- [ ] T16a Finance · Market charts -- [ ] T16b Finance · Technical analysis -- [ ] T16c Finance · Quotes & book -- [ ] T16d Finance · Trading -- [ ] T16e Finance · Markets & assets +- [x] T16a Finance · Market charts +- [x] T16b Finance · Technical analysis +- [x] T16c Finance · Quotes & book +- [x] T16d Finance · Trading +- [x] T16e Finance · Markets & assets - [ ] T17a Editor · Core - [ ] T17b Editor · Intelligence - [ ] T17c Editor · Search @@ -216,8 +216,16 @@ Per-component lines live in `tasks/`. Tags there: | T15b | 2 | PASS | | | T15c | 1 | FAIL | eight charts kept a pointed part past their data, and the Gantt today line ran past the timeline | | T15c | 2 | PASS | | -| T16a | 1 | — | pending review | -| T16b | 1 | — | pending review | -| T16c | 1 | — | pending review | -| T16d | 1 | — | pending review | -| T16e | 1 | — | pending review | +| T16a | 1 | FAIL | an unknown system zone read as UTC, the view window kept its span when data shrank, and the volume profile dropped narrow and flat candles | +| T16a | 2 | FAIL | synced charts moved their shared window once per chart for one append | +| T16a | 3 | FAIL | a lagging synced chart moved the shared window again on every redraw; fixed after the cap by tracking each chart's own count, verified by a red-green test, no fourth review | +| T16b | 1 | FAIL | Fibonacci labels spilled out of their pane | +| T16b | 2 | FAIL | Fibonacci labels still ran past the price pane's top | +| T16b | 3 | PASS | | +| T16c | 1 | FAIL | the DOM ladder lost its tick's decimals, and the tape flashed only once | +| T16c | 2 | FAIL | ladder prices lost places their tick needed | +| T16c | 3 | FAIL | a step under 1e-10 showed no places; fixed after the cap by keeping places by magnitude, verified by a red-green test, no fourth review | +| T16d | 1 | FAIL | typed numbers reached the ticket only on blur, so a press sent the old value | +| T16d | 2 | PASS | | +| T16e | 1 | FAIL | a smaller loss than expected read as a miss | +| T16e | 2 | PASS | | diff --git a/src/finance/market.rs b/src/finance/market.rs index 669e90c..e7d1bb9 100644 --- a/src/finance/market.rs +++ b/src/finance/market.rs @@ -40,7 +40,7 @@ pub enum ChartKind { pub struct ChartSync { pub(crate) visible: Option, pub(crate) hover: Option, - /// How many candles the shared window last moved for, so one append moves it once. + /// The most candles the shared window has moved for: an append moves it once, a lagging chart never. pub(crate) seen: usize, } @@ -230,37 +230,37 @@ impl RenderOnce for CandlestickChart { ..frame }; let total = self.candles.len(); - let held = match &self.sync { - Some(sync) => (sync.read(cx).visible, sync.read(cx).hover), - None => (stage.read(cx).visible, stage.read(cx).hover), - }; - let seen = match &self.sync { - Some(sync) => sync.read(cx).seen, - None => stage.read(cx).seen, + let own = stage.read(cx).seen; + let (held, shared) = match &self.sync { + Some(sync) => { + let sync = sync.read(cx); + ((sync.visible, sync.hover), sync.seen) + } + None => ((stage.read(cx).visible, stage.read(cx).hover), own), }; + let fresh = total.saturating_sub(own).min(total.saturating_sub(shared)); let visible = match held.0 { Some(visible) - if total > seen - && seen > 0 - && visible.start + visible.count >= seen as f64 - 0.5 => + if fresh > 0 + && own > 0 + && visible.start + visible.count >= (total - fresh) as f64 - 0.5 => { - visible.pan((total - seen) as f64, total) + visible.pan(fresh as f64, total) } Some(visible) => visible.fitted(total), None => Visible::latest(total, f64::from(main.w / pixels(sizes.candle)).max(1.0)), }; - if seen != total { - log::debug!("market chart: {seen} candles to {total}"); - let kept = held.0.map(|_| visible); + if own != total { + log::debug!("market chart: {own} candles to {total}"); + let shrunk = total < own; + let kept = held.0.filter(|_| fresh > 0 || shrunk).map(|_| visible); + stage.update(cx, |stage, _| stage.seen = total); match &self.sync { Some(sync) => sync.update(cx, |sync, _| { - sync.seen = total; + sync.seen = if shrunk { total } else { sync.seen.max(total) }; sync.visible = kept.or(sync.visible); }), - None => stage.update(cx, |stage, _| { - stage.seen = total; - stage.visible = kept.or(stage.visible); - }), + None => stage.update(cx, |stage, _| stage.visible = kept.or(stage.visible)), } } let range = visible.range(total); diff --git a/src/finance/stage.rs b/src/finance/stage.rs index 8199951..6e1eb5d 100644 --- a/src/finance/stage.rs +++ b/src/finance/stage.rs @@ -104,13 +104,17 @@ pub(crate) fn fit(low: f64, high: f64) -> ((f64, f64), Vec) { ((low, high), inside) } +/// Digits kept past a step's first; float noise sits near the sixteenth. +const DIGITS: usize = 6; + /// Places after the point that show every multiple of `step` exactly: none for whole steps, four for 0.0025. pub(crate) fn decimals(step: f64) -> usize { assert!( step.is_finite() && step > 0.0, "a step is positive, not {step}" ); - let text = format!("{step:.10}"); + let leading = (-step.log10().floor()).max(0.0) as usize; + let text = format!("{step:.*}", leading + DIGITS); let fraction = text.split_once('.').map_or("", |(_, fraction)| fraction); fraction.trim_end_matches('0').len() } @@ -207,5 +211,6 @@ mod tests { 1, "float noise from a difference drops away" ); + assert_eq!(decimals(1e-11), 11, "a tiny step keeps its places"); } } diff --git a/src/finance/tests.rs b/src/finance/tests.rs index 71588b4..1e405a0 100644 --- a/src/finance/tests.rs +++ b/src/finance/tests.rs @@ -2,7 +2,7 @@ use std::rc::Rc; use gpui::{ AppContext as _, Context, Entity, IntoElement, ParentElement, Render, Styled, TestAppContext, - Window, div, px, + VisualTestContext, Window, div, px, }; use jiff::Timestamp; @@ -21,7 +21,8 @@ fn candles(count: usize) -> Rc> { } struct Pair { - candles: Rc>, + first: Rc>, + second: Rc>, sync: Entity, } @@ -29,18 +30,19 @@ impl Render for Pair { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { div() .w(px(800.)) - .child(CandlestickChart::new("first", self.candles.clone()).sync(&self.sync)) - .child(CandlestickChart::new("second", self.candles.clone()).sync(&self.sync)) + .child(CandlestickChart::new("first", self.first.clone()).sync(&self.sync)) + .child(CandlestickChart::new("second", self.second.clone()).sync(&self.sync)) } } -#[gpui::test] -fn synced_charts_move_their_shared_window_once_per_append(cx: &mut TestAppContext) { +/// Two synced charts of 40 candles, their shared window held on the newest 20. +fn held(cx: &mut TestAppContext) -> (Entity, Entity, &mut VisualTestContext) { cx.update(Theme::init); let sync = cx.new(|_| ChartSync::default()); let shared = sync.clone(); let (view, cx) = cx.add_window_view(|_, _| Pair { - candles: candles(40), + first: candles(40), + second: candles(40), sync: shared, }); cx.run_until_parked(); @@ -53,14 +55,54 @@ fn synced_charts_move_their_shared_window_once_per_append(cx: &mut TestAppContex }); view.update(cx, |_, cx| cx.notify()); cx.run_until_parked(); + (view, sync, cx) +} + +fn start(sync: &Entity, cx: &mut VisualTestContext) -> f64 { + sync.read_with(cx, |sync, _| sync.visible.expect("held").start) +} + +#[gpui::test] +fn synced_charts_move_their_shared_window_once_per_append(cx: &mut TestAppContext) { + let (view, sync, cx) = held(cx); view.update(cx, |pair, cx| { - pair.candles = candles(45); + pair.first = candles(45); + pair.second = candles(45); cx.notify(); }); cx.run_until_parked(); - let start = sync.read_with(cx, |sync, _| sync.visible.expect("held").start); assert_eq!( - start, 25.0, + start(&sync, cx), + 25.0, "five new candles move the shared window five, not ten" ); } + +#[gpui::test] +fn a_lagging_chart_leaves_the_shared_window_alone(cx: &mut TestAppContext) { + let (view, sync, cx) = held(cx); + view.update(cx, |pair, cx| { + pair.second = candles(45); + cx.notify(); + }); + cx.run_until_parked(); + for _ in 0..2 { + view.update(cx, |_, cx| cx.notify()); + cx.run_until_parked(); + } + assert_eq!( + (start(&sync, cx), sync.read_with(cx, |sync, _| sync.seen)), + (25.0, 45), + "redraws while the first chart lags move nothing" + ); + view.update(cx, |pair, cx| { + pair.first = candles(45); + cx.notify(); + }); + cx.run_until_parked(); + assert_eq!( + start(&sync, cx), + 25.0, + "the lagging chart's append is already shown" + ); +}