use std::rc::Rc; use gpui::{ App, Div, ElementId, Entity, InteractiveElement, IntoElement, ParentElement, Refineable, RenderOnce, SharedString, StyleRefinement, Styled, Window, div, }; use jiff::tz::TimeZone; use super::{ candles::{Candle, heikin_ashi, profile}, draw::{Pen, Scene, drawing}, labels::{Axes, labels}, quotes::moves, series::{Drawn, Overlay, Study, overlay, study}, stage::{Visible, fit}, steer::{OnDraw, Stage, Steering}, tools::{Drawing, Tool}, }; use crate::{ charts::{Linear, Rect, measure}, theme::ActiveTheme, typography::format::system_zone, }; /// How a market chart draws its prices. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum ChartKind { #[default] Candles, /// Open, high, low and close as bars with ticks. Bars, /// Candles smoothed by averaging, which hold a trend's color longer. HeikinAshi, Line, Area, } /// What market charts that move together share: the candles in view and the one under the pointer. Create one entity and hand it to each chart. #[derive(Default)] pub struct ChartSync { pub(crate) visible: Option, pub(crate) hover: Option, /// The most candles the shared window has moved for: an append moves it once, a lagging chart never. 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. #[derive(IntoElement)] pub struct CandlestickChart { base: Div, id: ElementId, candles: Rc>, kind: ChartKind, title: Option, volume: bool, profile: bool, overlays: Vec, studies: Vec, alerts: Vec<(f64, SharedString)>, compare: Option<(SharedString, Rc>)>, sync: Option>, red_up: bool, zone: Option, drawings: Vec, tool: Option, on_draw: Option, } impl CandlestickChart { /// Candles oldest first, one per period. pub fn new(id: impl Into, candles: impl Into>>) -> Self { let candles = candles.into(); assert!( candles.windows(2).all(|pair| pair[0].time < pair[1].time), "candles run oldest first" ); Self { base: div(), id: id.into(), candles, kind: ChartKind::Candles, title: None, volume: false, profile: false, overlays: Vec::new(), studies: Vec::new(), alerts: Vec::new(), compare: None, sync: None, red_up: false, zone: None, drawings: Vec::new(), tool: None, on_draw: None, } } /// Marks the owner keeps, drawn over the prices. pub fn drawings(mut self, drawings: impl IntoIterator) -> Self { self.drawings = drawings.into_iter().collect(); self } /// A tool to draw with: a drag over the prices draws with it instead of moving through time. pub fn tool(mut self, tool: Option) -> Self { self.tool = tool; self } /// Gets each drawing a tool finishes, for the owner to keep. pub fn on_draw(mut self, handler: impl Fn(Drawing, &mut Window, &mut App) + 'static) -> Self { self.on_draw = Some(Rc::new(handler)); self } pub fn kind(mut self, kind: ChartKind) -> Self { self.kind = kind; self } /// A name read at the top left, such as a symbol and its interval. pub fn title(mut self, title: impl Into) -> Self { self.title = Some(title.into()); self } /// Volume as faint bars along the bottom. pub fn volume(mut self) -> Self { self.volume = true; self } /// The volume traded at each price in view, as bars from the right edge. pub fn profile(mut self) -> Self { self.profile = true; self } pub fn overlay(mut self, overlay: Overlay) -> Self { self.overlays.push(overlay); self } /// A study in a pane of its own under the prices. pub fn study(mut self, study: Study) -> Self { self.studies.push(study); self } /// A dashed level with its label at the price scale, as a price alert. pub fn alert(mut self, price: f64, label: impl Into) -> Self { assert!(price.is_finite(), "an alert needs a finite price"); self.alerts.push((price, label.into())); self } /// Another symbol's closes over the same periods, drawn from where this one starts in view so their moves compare. pub fn compare( mut self, name: impl Into, candles: impl Into>>, ) -> Self { let candles = candles.into(); assert_eq!( candles.len(), self.candles.len(), "a compared symbol needs a candle for each period" ); self.compare = Some((name.into(), candles)); self } /// Moves, zooms and points together with every chart given the same sync. pub fn sync(mut self, sync: &Entity) -> Self { self.sync = Some(sync.clone()); self } /// Rising prices in red and falling in green, as markets in China and Japan read them. pub fn red_up(mut self) -> Self { self.red_up = true; self } /// The time zone its times read in; the system's unless set. pub fn zone(mut self, zone: TimeZone) -> Self { self.zone = Some(zone); self } } impl Styled for CandlestickChart { fn style(&mut self) -> &mut StyleRefinement { self.base.style() } } impl RenderOnce for CandlestickChart { fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement { let zone = self .zone .clone() .unwrap_or_else(|| system_zone("CandlestickChart")); let sync = self.sync.clone(); let stage: Entity = window.use_keyed_state((self.id.clone(), "stage"), cx, |_, cx| Stage { _synced: sync .as_ref() .map(|sync| cx.observe(sync, |_, _, cx| cx.notify())), ..Stage::default() }); let theme = cx.theme(); let (sizes, rem) = (theme.chart(), window.rem_size()); let pixels = |length: gpui::Rems| f32::from(length.to_pixels(rem)); let bounds = stage.read(cx).bounds; let (gutter, foot, inset) = ( pixels(sizes.gutter) * 1.25, pixels(sizes.foot), pixels(sizes.inset), ); let frame = Rect { x: inset, y: inset, w: (f32::from(bounds.size.width) - inset - gutter).max(0.0), h: (f32::from(bounds.size.height) - inset - foot).max(0.0), }; let study_tall = pixels(sizes.height) * 0.3; let main = Rect { h: (frame.h - study_tall * self.studies.len() as f32).max(0.0), ..frame }; let total = self.candles.len(); 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 fresh > 0 && own > 0 && visible.start + visible.count >= (total - fresh) as f64 - 0.5 => { 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 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 = if shrunk { total } else { sync.seen.max(total) }; sync.visible = kept.or(sync.visible); }), None => stage.update(cx, |stage, _| stage.visible = kept.or(stage.visible)), } } let range = visible.range(total); let shown = Rc::new(if self.kind == ChartKind::HeikinAshi { heikin_ashi(&self.candles) } else { (*self.candles).clone() }); let drawn: Rc> = Rc::new( self.overlays .iter() .enumerate() .map(|(ix, kind)| overlay(*kind, &self.candles, ix + 1)) .collect(), ); let studied: Rc> = Rc::new( self.studies .iter() .map(|kind| study(*kind, &self.candles, 1)) .collect(), ); let compare = self.compare.as_ref().and_then(|(_, other)| { let first = range.clone().next()?; let ratio = self.candles[first].close / other[first].close; Some( other .iter() .map(|candle| Some(candle.close * ratio)) .collect::>(), ) }); let in_view = &shown[range.clone()]; let mut reach = in_view.iter().fold(None::<(f64, f64)>, |reach, candle| { let (low, high) = reach.unwrap_or((candle.low, candle.high)); Some((low.min(candle.low), high.max(candle.high))) }); for extra in drawn .iter() .filter_map(|drawn| drawn.reach(range.clone())) .chain(compare.iter().filter_map(|compare| { compare[range.clone()] .iter() .flatten() .fold(None::<(f64, f64)>, |reach, value| { let (low, high) = reach.unwrap_or((*value, *value)); Some((low.min(*value), high.max(*value))) }) })) { let (low, high) = reach.unwrap_or(extra); reach = Some((low.min(extra.0), high.max(extra.1))); } let ((low, high), ticks) = fit( reach.map_or(0.0, |reach| reach.0), reach.map_or(1.0, |reach| reach.1), ); let mut panes = vec![( main, Linear::new((low, high), (main.y + main.h, main.y)), ticks, )]; for (ix, drawn) in studied.iter().enumerate() { let pane = Rect { x: frame.x, y: main.y + main.h + study_tall * ix as f32, w: frame.w, h: study_tall, }; let inner = Rect { y: pane.y + inset, h: (pane.h - inset * 2.0).max(0.0), ..pane }; let (domain, ticks) = match drawn.reach(range.clone()) { Some(range) if drawn.range.is_some() => (range, drawn.guides.clone()), Some((low, high)) => fit(low, high), None => ((0.0, 1.0), Vec::new()), }; panes.push(( pane, Linear::new(domain, (inner.y + inner.h, inner.y)), ticks, )); } let (rise, fall) = moves(self.red_up, cx); let pointer = stage.read(cx).pointer; let hover = held.1.filter(|ix| *ix >= 0 && (*ix as usize) < total); let row = pointer.and_then(|(_, y)| { panes .iter() .position(|(pane, _, _)| y >= pane.y && y <= pane.y + pane.h) .map(|pane| (pane, y)) }); let last = self .candles .last() .map(|candle| (candle.close, candle.rose())); let volume = self.volume.then(|| { in_view .iter() .map(|candle| candle.volume) .fold(0.0, f64::max) }); let profiled = if self.profile { profile(in_view, (low, high), 24) } else { Vec::new() }; let sketch = stage.read(cx).sketch; let marks: Vec = self .drawings .iter() .copied() .chain(sketch.map(|(tool, from, to)| Drawing::of(tool, from, to))) .collect(); let price = panes[0].1; let axes = Axes { panes: panes.clone(), visible, range: range.clone(), hover, row, zone: zone.clone(), title: self.title.clone(), compare: self.compare.as_ref().map(|(name, _)| name.clone()), drawings: marks.clone(), }; let words = labels( &axes, (&shown, &self.candles), (&drawn, &studied), (&self.alerts, last), (rise, fall), window, cx, ); let theme = cx.theme(); let scene = Scene { panes, visible, range, candles: shown, kind: self.kind, volume, profile: profiled, overlays: drawn, studies: studied, compare, last, alerts: self.alerts.iter().map(|(price, _)| *price).collect(), cross: hover.map(|ix| (ix, row)), drawings: marks, pen: Pen { palette: theme.colors.clone(), rise, fall, stroke: sizes.stroke.to_pixels(rem), hairline: sizes.hairline, }, }; let height = sizes.height + sizes.height * 0.3 * self.studies.len() as f32; let mut root = div() .debug_selector(|| "chart-root".into()) .relative() .h(height); root.style().refine(self.base.style()); let steering = Steering { stage: stage.clone(), sync: self.sync.clone(), visible, total, main, price, tool: self.tool, on_draw: self.on_draw.clone(), }; steering .attach(root.id((self.id.clone(), "market"))) .child(drawing(scene)) .children(words) .child(measure(stage, |stage| &mut stage.bounds)) } }