T16 round 2 fixes: synced charts move their shared window once per append, Fibonacci labels sit in a box clipped to the price pane and flip below near its top, and prices show every place their tick needs
This commit is contained in:
5 files changed
+134
-25
No files matched your search
+36
-13
@@ -1,6 +1,9 @@
|
|||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
||||||
use gpui::{App, Div, FontWeight, Hsla, ParentElement, Pixels, SharedString, Styled, Window, div};
|
use gpui::{
|
||||||
|
App, Div, FontWeight, Hsla, ParentElement, Pixels, SharedString, Styled, Window, div,
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
use jiff::tz::TimeZone;
|
use jiff::tz::TimeZone;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@@ -12,7 +15,7 @@ use super::{
|
|||||||
use crate::{
|
use crate::{
|
||||||
charts::{Linear, Rect, tint},
|
charts::{Linear, Rect, tint},
|
||||||
theme::{ActiveTheme, Radius, TextSize},
|
theme::{ActiveTheme, Radius, TextSize},
|
||||||
typography::{format, tabular},
|
typography::{LEADING, format, tabular},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Where a market chart's words go: its panes and their scales, what is in view, the candle and row under the pointer, the zone its times read in, its title, and a compared symbol's name.
|
/// Where a market chart's words go: its panes and their scales, what is in view, the candle and row under the pointer, the zone its times read in, its title, and a compared symbol's name.
|
||||||
@@ -220,6 +223,8 @@ pub(crate) fn labels(
|
|||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let tall = f32::from(small.to_pixels(rem)) * LEADING;
|
||||||
|
let mut levels = Vec::new();
|
||||||
for drawing in &axes.drawings {
|
for drawing in &axes.drawings {
|
||||||
let Drawing::Fib(from, to) = *drawing else {
|
let Drawing::Fib(from, to) = *drawing else {
|
||||||
continue;
|
continue;
|
||||||
@@ -228,30 +233,48 @@ pub(crate) fn labels(
|
|||||||
if x > main.x + main.w {
|
if x > main.x + main.w {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let x = x.max(main.x);
|
let left = (x - main.x).max(0.0);
|
||||||
let inside = |y: f32| y >= main.y && y <= main.y + main.h;
|
for (share, value) in Drawing::levels(from, to) {
|
||||||
for (share, value) in Drawing::levels(from, to)
|
let y = price.at(value) - main.y;
|
||||||
.into_iter()
|
if !(0.0..=main.h).contains(&y) {
|
||||||
.filter(|(_, value)| inside(price.at(*value)))
|
continue;
|
||||||
{
|
}
|
||||||
let words = format!(
|
let words = format!(
|
||||||
"{} {}",
|
"{} {}",
|
||||||
format::percent(share, 1, false),
|
format::percent(share, 1, false),
|
||||||
format::number(value, digits, format::Separators::EN)
|
format::number(value, digits, format::Separators::EN)
|
||||||
);
|
);
|
||||||
out.push(
|
let above = y >= tall;
|
||||||
|
levels.push(
|
||||||
div()
|
div()
|
||||||
.absolute()
|
.absolute()
|
||||||
.left(Pixels::from(x))
|
.left(Pixels::from(left))
|
||||||
.top(Pixels::from(price.at(value)))
|
.top(Pixels::from(y))
|
||||||
.h_0()
|
.h_0()
|
||||||
.flex()
|
.flex()
|
||||||
.items_end()
|
|
||||||
.pl_1()
|
.pl_1()
|
||||||
.child(tabular(text(words, colors.fg_muted)).pb_0p5()),
|
.when(above, |label| label.items_end())
|
||||||
|
.when(!above, |label| label.items_start())
|
||||||
|
.child(
|
||||||
|
tabular(text(words, colors.fg_muted))
|
||||||
|
.when(above, |words| words.pb_0p5())
|
||||||
|
.when(!above, |words| words.pt_0p5()),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !levels.is_empty() {
|
||||||
|
out.push(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.left(Pixels::from(main.x))
|
||||||
|
.top(Pixels::from(main.y))
|
||||||
|
.w(Pixels::from(main.w))
|
||||||
|
.h(Pixels::from(main.h))
|
||||||
|
.overflow_hidden()
|
||||||
|
.children(levels),
|
||||||
|
);
|
||||||
|
}
|
||||||
let pill = |y: f32, words: String, (fill, ink): (Hsla, Hsla)| {
|
let pill = |y: f32, words: String, (fill, ink): (Hsla, Hsla)| {
|
||||||
let body = tabular(div())
|
let body = tabular(div())
|
||||||
.px_1p5()
|
.px_1p5()
|
||||||
|
|||||||
+16
-7
@@ -40,6 +40,8 @@ pub enum ChartKind {
|
|||||||
pub struct ChartSync {
|
pub struct ChartSync {
|
||||||
pub(crate) visible: Option<Visible>,
|
pub(crate) visible: Option<Visible>,
|
||||||
pub(crate) hover: Option<i64>,
|
pub(crate) hover: Option<i64>,
|
||||||
|
/// How many candles the shared window last moved for, so one append moves it once.
|
||||||
|
pub(crate) seen: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A market's prices over time as candles, bars or a line, on a price scale at the right. Drag or scroll sideways to move through time, hold Cmd and scroll to zoom, and double-press to return to the newest; the crosshair reads each candle.
|
/// A market's prices over time as candles, bars or a line, on a price scale at the right. Drag or scroll sideways to move through time, hold Cmd and scroll to zoom, and double-press to return to the newest; the crosshair reads each candle.
|
||||||
@@ -232,7 +234,10 @@ impl RenderOnce for CandlestickChart {
|
|||||||
Some(sync) => (sync.read(cx).visible, sync.read(cx).hover),
|
Some(sync) => (sync.read(cx).visible, sync.read(cx).hover),
|
||||||
None => (stage.read(cx).visible, stage.read(cx).hover),
|
None => (stage.read(cx).visible, stage.read(cx).hover),
|
||||||
};
|
};
|
||||||
let seen = stage.read(cx).seen;
|
let seen = match &self.sync {
|
||||||
|
Some(sync) => sync.read(cx).seen,
|
||||||
|
None => stage.read(cx).seen,
|
||||||
|
};
|
||||||
let visible = match held.0 {
|
let visible = match held.0 {
|
||||||
Some(visible)
|
Some(visible)
|
||||||
if total > seen
|
if total > seen
|
||||||
@@ -246,12 +251,16 @@ impl RenderOnce for CandlestickChart {
|
|||||||
};
|
};
|
||||||
if seen != total {
|
if seen != total {
|
||||||
log::debug!("market chart: {seen} candles to {total}");
|
log::debug!("market chart: {seen} candles to {total}");
|
||||||
stage.update(cx, |stage, _| stage.seen = total);
|
let kept = held.0.map(|_| visible);
|
||||||
if held.0.is_some() {
|
match &self.sync {
|
||||||
match &self.sync {
|
Some(sync) => sync.update(cx, |sync, _| {
|
||||||
Some(sync) => sync.update(cx, |sync, _| sync.visible = Some(visible)),
|
sync.seen = total;
|
||||||
None => stage.update(cx, |stage, _| stage.visible = Some(visible)),
|
sync.visible = kept.or(sync.visible);
|
||||||
}
|
}),
|
||||||
|
None => stage.update(cx, |stage, _| {
|
||||||
|
stage.seen = total;
|
||||||
|
stage.visible = kept.or(stage.visible);
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let range = visible.range(total);
|
let range = visible.range(total);
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ mod stage;
|
|||||||
mod steer;
|
mod steer;
|
||||||
mod symbols;
|
mod symbols;
|
||||||
mod tape;
|
mod tape;
|
||||||
|
#[cfg(all(test, feature = "test-support"))]
|
||||||
|
mod tests;
|
||||||
mod tools;
|
mod tools;
|
||||||
mod trade;
|
mod trade;
|
||||||
mod wallet;
|
mod wallet;
|
||||||
|
|||||||
+14
-5
@@ -104,12 +104,15 @@ pub(crate) fn fit(low: f64, high: f64) -> ((f64, f64), Vec<f64>) {
|
|||||||
((low, high), inside)
|
((low, high), inside)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Places after the point that ticks `step` apart need: none for whole steps, two for cents.
|
/// 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 {
|
pub(crate) fn decimals(step: f64) -> usize {
|
||||||
if step >= 1.0 || step <= 0.0 {
|
assert!(
|
||||||
return 0;
|
step.is_finite() && step > 0.0,
|
||||||
}
|
"a step is positive, not {step}"
|
||||||
(-step.log10() - 1e-9).ceil().max(0.0) as usize
|
);
|
||||||
|
let text = format!("{step:.10}");
|
||||||
|
let fraction = text.split_once('.').map_or("", |(_, fraction)| fraction);
|
||||||
|
fraction.trim_end_matches('0').len()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -198,5 +201,11 @@ mod tests {
|
|||||||
),
|
),
|
||||||
(0, 1, 2, 3)
|
(0, 1, 2, 3)
|
||||||
);
|
);
|
||||||
|
assert_eq!(decimals(0.0025), 4, "a quarter of a cent needs four places");
|
||||||
|
assert_eq!(
|
||||||
|
decimals(110.2 - 110.0),
|
||||||
|
1,
|
||||||
|
"float noise from a difference drops away"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use gpui::{
|
||||||
|
AppContext as _, Context, Entity, IntoElement, ParentElement, Render, Styled, TestAppContext,
|
||||||
|
Window, div, px,
|
||||||
|
};
|
||||||
|
use jiff::Timestamp;
|
||||||
|
|
||||||
|
use crate::finance::{CandlestickChart, ChartSync, candles::Candle, stage::Visible};
|
||||||
|
use crate::theme::Theme;
|
||||||
|
|
||||||
|
fn candles(count: usize) -> Rc<Vec<Candle>> {
|
||||||
|
Rc::new(
|
||||||
|
(0..count)
|
||||||
|
.map(|ix| {
|
||||||
|
let time = Timestamp::from_second(ix as i64 * 86_400).expect("a day");
|
||||||
|
Candle::new(time, (10.0, 11.0, 9.0, 10.0 + ix as f64 * 0.01), 100.0)
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Pair {
|
||||||
|
candles: Rc<Vec<Candle>>,
|
||||||
|
sync: Entity<ChartSync>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Pair {
|
||||||
|
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn synced_charts_move_their_shared_window_once_per_append(cx: &mut TestAppContext) {
|
||||||
|
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),
|
||||||
|
sync: shared,
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
sync.update(cx, |sync, cx| {
|
||||||
|
sync.visible = Some(Visible {
|
||||||
|
start: 20.0,
|
||||||
|
count: 20.0,
|
||||||
|
});
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
view.update(cx, |_, cx| cx.notify());
|
||||||
|
cx.run_until_parked();
|
||||||
|
view.update(cx, |pair, cx| {
|
||||||
|
pair.candles = 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,
|
||||||
|
"five new candles move the shared window five, not ten"
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user