1use super::{
12 ArrowStyle, BarsPatternConfig, ChartPoint, DrawingToolType, ElliottDegree, FibonacciConfig,
13 FontWeight, GANN_SQUARE_UNITS, GannConfig, GhostFeedConfig, ImageConfig, PitchforkConfig,
14 PositionSize, PositionSizing, RegressionTrendConfig, TimeframeVisibility, VwapConfig,
15};
16
17use crate::data::VolumeProfileConfig;
18use crate::model::{AlertSource, LineStyle, PostLink};
19use crate::scales::PriceScaleMode;
20use serde::{Deserialize, Serialize};
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
43#[serde(remote = "Self")]
45pub struct Drawing {
46 #[serde(skip)]
48 pub id: usize,
49 #[serde(skip)]
52 pub stored_id: Option<String>,
53 #[serde(skip)]
56 pub created_at: Option<i64>,
57 #[serde(skip)]
59 pub notes: Option<String>,
60 #[serde(default)]
62 pub label: Option<String>,
63 #[serde(default)]
66 pub pane: Option<usize>,
67 #[serde(default = "enabled")]
69 pub selectable: bool,
70 #[serde(default = "enabled")]
72 pub saveable: bool,
73 #[serde(default = "enabled")]
75 pub listed: bool,
76 #[serde(default)]
78 pub group: Option<String>,
79 #[serde(default)]
82 pub folder: Vec<String>,
83 #[serde(default)]
85 pub custom_tool: Option<String>,
86 #[serde(default)]
88 pub custom_settings: serde_json::Value,
89 pub tool_type: DrawingToolType,
91 #[serde(skip)]
93 pub points: Vec<egui::Pos2>,
94 #[serde(skip)]
96 pub chart_points: Vec<ChartPoint>,
97 #[serde(skip)]
101 pub level_ys: Vec<f32>,
102 #[serde(default)]
107 pub screen_anchor: Option<[f32; 2]>,
108 pub color: [u8; 4],
111 pub stroke_width: f32,
113 #[serde(skip)]
115 pub completed: bool,
116 pub text: Option<String>,
121 #[serde(default)]
126 pub post: Option<PostLink>,
127
128 pub line_style: LineStyle,
131 pub fill_color: Option<[u8; 4]>,
134 pub locked: bool,
136 pub visible: bool,
138 pub font_size: f32,
140 pub font_weight: FontWeight,
142 #[serde(default)]
144 pub italic: bool,
145 #[serde(default)]
148 pub text_color: Option<[u8; 4]>,
149 #[serde(default = "enabled")]
153 pub background: bool,
154 #[serde(default = "enabled")]
156 pub border: bool,
157 #[serde(default)]
159 pub border_color: Option<[u8; 4]>,
160 pub extend_left: bool,
162 pub extend_right: bool,
164 pub arrow_start_style: ArrowStyle,
166 pub arrow_end_style: ArrowStyle,
168 pub timeframe_visibility: TimeframeVisibility,
170 #[serde(default = "enabled")]
173 pub price_axis_label: bool,
174 #[serde(default = "enabled")]
177 pub time_axis_label: bool,
178
179 pub position_size: Option<PositionSize>,
182 #[serde(default)]
184 pub position_sizing: PositionSizing,
185 #[serde(default)]
187 pub position_target_color: Option<[u8; 4]>,
188 #[serde(default)]
190 pub position_stop_color: Option<[u8; 4]>,
191 pub fib_config: Option<FibonacciConfig>,
193 pub volume_profile: Option<VolumeProfileConfig>,
195 pub regression_trend: Option<RegressionTrendConfig>,
197 #[serde(default)]
199 pub vwap: Option<VwapConfig>,
200
201 #[serde(default)]
204 pub gann: Option<GannConfig>,
205 #[serde(default)]
207 pub ghost_feed: Option<GhostFeedConfig>,
208 #[serde(default)]
210 pub channel_midline: bool,
211 #[serde(default)]
213 pub image: Option<ImageConfig>,
214 #[serde(default)]
217 pub closed: bool,
218 #[serde(default)]
220 pub pitchfork: Option<PitchforkConfig>,
221 #[serde(default)]
223 pub elliott_degree: ElliottDegree,
224
225 #[serde(default)]
228 pub bars_pattern: Option<BarsPatternConfig>,
229
230 pub z_order: i32,
233}
234
235fn enabled() -> bool {
236 true
237}
238
239pub(super) const BLUE: [u8; 4] = [41, 98, 255, 255];
240pub(super) const RED: [u8; 4] = [242, 54, 69, 255];
241pub(super) const ORANGE: [u8; 4] = [255, 152, 0, 255];
242pub(super) const GREEN: [u8; 4] = [76, 175, 80, 255];
243pub(super) const TEAL: [u8; 4] = [8, 153, 129, 255];
244pub(super) const CYAN: [u8; 4] = [0, 188, 212, 255];
245pub(super) const PINK: [u8; 4] = [233, 30, 99, 255];
246pub(super) const PURPLE: [u8; 4] = [156, 39, 176, 255];
247const DEEP_PURPLE: [u8; 4] = [103, 58, 183, 255];
248pub(super) const GREY: [u8; 4] = [128, 128, 128, 255];
249const WHITE: [u8; 4] = [255, 255, 255, 255];
250
251const fn tint(color: [u8; 4], alpha: u8) -> [u8; 4] {
253 [color[0], color[1], color[2], alpha]
254}
255
256struct DefaultStyle {
258 color: [u8; 4],
259 width: f32,
260 line_style: LineStyle,
261 fill: Option<[u8; 4]>,
262 font_size: f32,
263}
264
265const LINE: DefaultStyle = DefaultStyle {
267 color: BLUE,
268 width: 2.0,
269 line_style: LineStyle::Solid,
270 fill: None,
271 font_size: 14.0,
272};
273
274fn default_style(tool: DrawingToolType) -> DefaultStyle {
277 use DrawingToolType as T;
278 let filled = |color: [u8; 4], alpha: u8| DefaultStyle {
279 color,
280 fill: Some(tint(color, alpha)),
281 ..LINE
282 };
283 let colored = |color: [u8; 4]| DefaultStyle { color, ..LINE };
284 match tool {
285 T::HorizontalLine | T::HorizontalRay | T::CrossLine => DefaultStyle {
286 font_size: 12.0,
287 ..LINE
288 },
289 T::ParallelChannel => filled(BLUE, 51),
290 T::RegressionTrend => DefaultStyle {
292 color: RED,
293 fill: Some(tint(BLUE, 51)),
294 ..LINE
295 },
296 T::FlatTopBottom => filled(ORANGE, 51),
297 T::DisjointChannel | T::Triangle => filled(TEAL, 51),
298 T::DoubleCurve | T::ThreeDrivesPattern => colored(DEEP_PURPLE),
299 T::Rect => filled(PURPLE, 51),
300 T::RotatedRect => filled(GREEN, 51),
301 T::Circle | T::Ellipse => filled(RED, 51),
302 T::Polyline | T::Brush => colored(CYAN),
304 T::Arc => colored(PINK),
305 T::Sector => DefaultStyle {
306 color: [156, 156, 156, 255],
307 width: 1.0,
308 fill: Some(tint(BLUE, 51)),
309 ..LINE
310 },
311 T::Highlighter => DefaultStyle {
312 color: tint(RED, 51),
313 width: 20.0,
314 ..LINE
315 },
316 T::ArrowMarker => DefaultStyle {
317 color: [30, 83, 229, 255],
318 font_size: 16.0,
319 ..LINE
320 },
321 T::ArrowMarkUp => colored(TEAL),
322 T::ArrowMarkDown => colored([204, 47, 60, 255]),
323 T::Pitchfork
324 | T::SchiffPitchfork
325 | T::ModifiedSchiffPitchfork
326 | T::InsidePitchfork
327 | T::Pitchfan => colored(RED),
328 T::FibonacciRetracement
330 | T::FibonacciExtension
331 | T::FibonacciChannel
332 | T::TrendBasedFibTime
333 | T::FibonacciCircles
334 | T::FibonacciSpeedResistanceArcs => DefaultStyle {
335 color: GREY,
336 line_style: LineStyle::Dashed,
337 ..LINE
338 },
339 T::FibonacciTimeZones => DefaultStyle {
340 color: GREY,
341 width: 1.0,
342 line_style: LineStyle::Dashed,
343 ..LINE
344 },
345 T::FibonacciWedge | T::FibonacciSpeedFan => colored(GREY),
346 T::FibonacciSpiral => DefaultStyle {
347 color: CYAN,
348 width: 1.0,
349 ..LINE
350 },
351 T::GhostFeed => DefaultStyle {
352 color: GREY,
353 width: 1.0,
354 ..LINE
355 },
356 T::XABCDPattern | T::CypherPattern => filled(BLUE, 38),
357 T::HeadAndShoulders => filled(TEAL, 38),
358 T::ABCDPattern | T::SineLine => colored(TEAL),
359 T::TrianglePattern => filled(DEEP_PURPLE, 38),
360 T::ElliottImpulse
361 | T::ElliottCorrection
362 | T::ElliottTriangle
363 | T::ElliottDoubleCombo
364 | T::ElliottTripleCombo => colored([61, 133, 198, 255]),
365 T::CyclicLines => colored([144, 202, 249, 255]),
366 T::TimeCycles => DefaultStyle {
367 color: [21, 153, 128, 255],
368 fill: Some([106, 168, 79, 128]),
369 ..LINE
370 },
371 T::Measure | T::PriceRange => DefaultStyle {
373 font_size: 12.0,
374 ..LINE
375 },
376 T::DateRange | T::DateAndPriceRange => DefaultStyle {
377 font_size: 12.0,
378 ..filled(BLUE, 102)
379 },
380 T::TextLabel | T::AnchoredText => DefaultStyle {
382 fill: Some(tint(BLUE, 64)),
383 ..LINE
384 },
385 T::Note => filled(BLUE, 179),
386 T::Callout => filled([0, 151, 167, 255], 179),
387 T::PriceNote | T::Signpost => DefaultStyle {
388 font_size: 12.0,
389 ..LINE
390 },
391 T::Comment => DefaultStyle {
392 font_size: 16.0,
393 ..LINE
394 },
395 T::Table => colored([219, 219, 219, 255]),
396 T::FontIcon => DefaultStyle {
397 font_size: 40.0,
398 ..LINE
399 },
400 T::Sticker => DefaultStyle {
401 font_size: 110.0,
402 ..LINE
403 },
404 _ => LINE,
405 }
406}
407
408impl Drawing {
409 pub fn new(id: usize, tool_type: DrawingToolType) -> Self {
411 let style = default_style(tool_type);
412 let plain_text = matches!(
414 tool_type,
415 DrawingToolType::TextLabel | DrawingToolType::AnchoredText
416 );
417 Self {
418 id,
419 stored_id: None,
420 created_at: None,
421 notes: None,
422 label: None,
423 selectable: true,
424 saveable: true,
425 listed: true,
426 group: None,
427 folder: Vec::new(),
428 tool_type,
429 custom_tool: None,
430 custom_settings: serde_json::Value::Null,
431 points: Vec::new(),
432 chart_points: Vec::new(),
433 level_ys: Vec::new(),
434 screen_anchor: None,
435 pane: None,
436 color: style.color,
437 stroke_width: style.width,
438 completed: false,
439 text: (tool_type == DrawingToolType::Sticker).then(|| "sticker:bull".into()),
440 post: None,
441 line_style: style.line_style,
443 fill_color: style.fill,
444 locked: false,
445 visible: true,
446 font_size: style.font_size,
447 font_weight: if tool_type == DrawingToolType::ArrowMarker {
448 FontWeight::Bold
449 } else {
450 FontWeight::Normal
451 },
452 italic: false,
453 text_color: matches!(tool_type, DrawingToolType::Note | DrawingToolType::Callout)
454 .then_some(WHITE),
455 background: !plain_text,
456 border: !plain_text,
457 border_color: plain_text.then_some([112, 112, 112, 255]),
458 extend_left: tool_type == DrawingToolType::ExtendedLine,
459 extend_right: matches!(
460 tool_type,
461 DrawingToolType::Ray | DrawingToolType::ExtendedLine
462 ),
463 arrow_start_style: ArrowStyle::None,
464 arrow_end_style: if matches!(tool_type, DrawingToolType::Arrow | DrawingToolType::Path)
465 {
466 ArrowStyle::Arrow
467 } else {
468 ArrowStyle::None
469 },
470 timeframe_visibility: TimeframeVisibility::all(),
471 price_axis_label: true,
472 time_axis_label: true,
473 position_size: None,
474 position_sizing: PositionSizing::default(),
475 position_target_color: None,
476 position_stop_color: None,
477 fib_config: matches!(
478 tool_type,
479 DrawingToolType::FibonacciRetracement
480 | DrawingToolType::FibonacciExtension
481 | DrawingToolType::FibonacciChannel
482 | DrawingToolType::FibonacciWedge
483 | DrawingToolType::FibonacciTimeZones
484 | DrawingToolType::FibonacciSpeedFan
485 | DrawingToolType::TrendBasedFibTime
486 | DrawingToolType::FibonacciCircles
487 | DrawingToolType::FibonacciSpeedResistanceArcs
488 )
489 .then(|| FibonacciConfig::for_tool(tool_type)),
490 volume_profile: matches!(
491 tool_type,
492 DrawingToolType::FixedRangeVolumeProfile | DrawingToolType::AnchoredVolumeProfile
493 )
494 .then(VolumeProfileConfig::drawing_default),
495 regression_trend: None,
496 vwap: None,
497 pitchfork: matches!(
498 tool_type,
499 DrawingToolType::Pitchfork
500 | DrawingToolType::SchiffPitchfork
501 | DrawingToolType::ModifiedSchiffPitchfork
502 | DrawingToolType::InsidePitchfork
503 )
504 .then(PitchforkConfig::default),
505 elliott_degree: ElliottDegree::default(),
506 channel_midline: tool_type == DrawingToolType::ParallelChannel,
507 image: None,
508 closed: false,
509 gann: None,
510 ghost_feed: None,
511 bars_pattern: None,
512 z_order: 0,
514 }
515 }
516
517 pub fn custom(tool: impl Into<String>) -> Self {
520 let mut drawing = Self::new(0, DrawingToolType::Polyline);
521 drawing.custom_tool = Some(tool.into());
522 drawing
523 }
524
525 pub fn with_color(id: usize, tool_type: DrawingToolType, color: [u8; 4]) -> Self {
528 let mut drawing = Self::new(id, tool_type);
529 if let Some(fill) = &mut drawing.fill_color
531 && fill[..3] == drawing.color[..3]
532 {
533 fill[..3].copy_from_slice(&color[..3]);
534 }
535 drawing.color = color;
536 drawing
537 }
538
539 pub fn with_text(id: usize, text: String) -> Self {
541 let mut drawing = Self::new(id, DrawingToolType::TextLabel);
542 drawing.text = Some(text);
543 drawing.arrow_end_style = ArrowStyle::None;
544 drawing
545 }
546
547 pub fn update_screen_coords<F, G>(&mut self, bar_to_x: F, price_to_y: G)
551 where
552 F: Fn(f32) -> f32,
553 G: Fn(f64) -> f32,
554 {
555 self.points.clear();
556 for cp in &self.chart_points {
557 let x = bar_to_x(cp.bar_idx);
558 let y = price_to_y(cp.price);
559 self.points.push(egui::Pos2::new(x, y));
560 }
561 self.level_ys = match (self.tool_type, &self.chart_points[..]) {
562 (DrawingToolType::GannFixed, [a, b, ..]) => {
564 vec![price_to_y(
565 a.price + (b.price - a.price) * f64::from(GANN_SQUARE_UNITS),
566 )]
567 }
568 _ => self
569 .fib_levels()
570 .map(|(_, prices)| prices.into_iter().map(&price_to_y).collect())
571 .unwrap_or_default(),
572 };
573 }
574
575 pub fn update_pane_coords(&mut self, pane: egui::Rect) {
579 if self.tool_type != DrawingToolType::AnchoredText
580 || !pane.is_finite()
581 || pane.width() <= 0.0
582 || pane.height() <= 0.0
583 {
584 return;
585 }
586 if let Some(pos) = self.points.first_mut() {
587 let anchor = self.screen_anchor.get_or_insert_with(|| {
588 [
589 (pos.x - pane.left()) / pane.width(),
590 (pos.y - pane.top()) / pane.height(),
591 ]
592 });
593 *pos = pane.min + egui::vec2(anchor[0] * pane.width(), anchor[1] * pane.height());
594 }
595 }
596
597 pub(crate) fn fib_levels(&self) -> Option<(std::borrow::Cow<'_, FibonacciConfig>, Vec<f64>)> {
600 let extension = match self.tool_type {
601 DrawingToolType::FibonacciRetracement => false,
602 DrawingToolType::FibonacciExtension => true,
603 _ => return None,
604 };
605 let [start, end, ..] = &self.chart_points[..] else {
606 return None;
607 };
608 let config = self.fibonacci_config();
609 let origin = self.chart_points.get(2).unwrap_or(start).price;
610 let prices = config
611 .level_prices(start.price, end.price, extension, origin)
612 .collect();
613 Some((config, prices))
614 }
615
616 pub fn alert_levels(&self, bar: f64, scale: PriceScaleMode) -> Vec<(usize, String, f64)> {
629 use DrawingToolType as T;
630 let named = |names: &[&str], prices: &[f64]| {
631 names
632 .iter()
633 .zip(prices)
634 .enumerate()
635 .map(|(level, (name, price))| (level, (*name).to_owned(), *price))
636 .collect::<Vec<_>>()
637 };
638 let log = scale == PriceScaleMode::Logarithmic;
639 let to_axis = |price: f64| if log { price.ln() } else { price };
640 let from_axis = |value: f64| if log { value.exp() } else { value };
641 let along = |a: &ChartPoint, b: &ChartPoint, bar: f64| {
643 let (from, to) = (to_axis(a.price), to_axis(b.price));
644 from + (bar - f64::from(a.bar_idx)) * (to - from) / f64::from(b.bar_idx - a.bar_idx)
645 };
646 let levels = match (self.tool_type, &self.chart_points[..]) {
647 (T::HorizontalLine | T::HorizontalRay, [level, ..]) => named(&["Line"], &[level.price]),
648 (
649 T::TrendLine | T::Ray | T::ExtendedLine | T::InfoLine | T::TrendAngle,
650 [start, end, ..],
651 ) => named(&["Line"], &[from_axis(along(start, end, bar))]),
652 (T::ParallelChannel, [start, end, through, ..]) => {
654 let shift = to_axis(through.price) - along(start, end, f64::from(through.bar_idx));
655 named(
656 &["Line", "Parallel line"],
657 &[
658 from_axis(along(start, end, bar)),
659 from_axis(along(start, end, bar) + shift),
660 ],
661 )
662 }
663 (T::FlatTopBottom, [start, end, flat, ..]) => named(
664 &["Line", "Flat line"],
665 &[from_axis(along(start, end, bar)), flat.price],
666 ),
667 (T::DisjointChannel, [start, end, from, ..]) => {
669 let rise = to_axis(end.price) - to_axis(start.price);
670 let run = f64::from(end.bar_idx - start.bar_idx);
671 named(
672 &["Line", "Second line"],
673 &[
674 from_axis(along(start, end, bar)),
675 from_axis(
676 to_axis(from.price) - (bar - f64::from(from.bar_idx)) * rise / run,
677 ),
678 ],
679 )
680 }
681 (
682 T::Pitchfork
683 | T::SchiffPitchfork
684 | T::ModifiedSchiffPitchfork
685 | T::InsidePitchfork
686 | T::Pitchfan,
687 _,
688 ) => self.fork_alert_levels(bar, scale),
689 (T::Rect, [a, b, ..]) => named(
690 &["Top", "Bottom"],
691 &[a.price.max(b.price), a.price.min(b.price)],
692 ),
693 (T::LongPos | T::ShortPos, [entry, target, rest @ ..]) => named(
694 &["Entry", "Target", "Stop"],
695 &[
696 entry.price,
697 target.price,
698 rest.first()
700 .map_or(2.0 * entry.price - target.price, |stop| stop.price),
701 ],
702 ),
703 _ => self
704 .fib_levels()
705 .map(|(config, prices)| {
706 config
707 .levels
708 .iter()
709 .enumerate()
710 .filter(|(_, level)| level.visible)
711 .zip(prices)
712 .map(|((index, level), price)| {
713 (index, level.label(config.levels_as_percent), price)
714 })
715 .collect()
716 })
717 .unwrap_or_default(),
718 };
719 if levels.iter().all(|(_, _, price)| price.is_finite()) {
720 levels
721 } else {
722 Vec::new()
723 }
724 }
725
726 pub fn alert_source(&self, level: usize) -> AlertSource {
731 let id = self.stable_id();
732 AlertSource::Drawing(if level == 0 {
733 id.into_owned()
734 } else {
735 format!("{id}#{level}")
736 })
737 }
738
739 pub fn alert_level(&self, source: &AlertSource) -> Option<usize> {
741 let AlertSource::Drawing(source) = source else {
742 return None;
743 };
744 let id = self.stable_id();
745 if *source == id {
746 return Some(0);
747 }
748 source
749 .strip_prefix(id.as_ref())?
750 .strip_prefix('#')?
751 .parse()
752 .ok()
753 .filter(|level| *level > 0)
754 }
755
756 fn stable_id(&self) -> std::borrow::Cow<'_, str> {
757 let id: std::borrow::Cow<'_, str> = self
758 .stored_id
759 .as_deref()
760 .map_or_else(|| self.id.to_string().into(), Into::into);
761 if id.contains('%') || id.contains('#') {
762 id.replace('%', "%25").replace('#', "%23").into()
763 } else {
764 id
765 }
766 }
767
768 pub fn add_point_with_chart_coords<F, G>(
770 &mut self,
771 screen_point: egui::Pos2,
772 x_to_bar: F,
773 y_to_price: G,
774 ) where
775 F: Fn(f32) -> f32,
776 G: Fn(f32) -> f64,
777 {
778 let bar_idx = x_to_bar(screen_point.x);
779 let price = y_to_price(screen_point.y);
780 self.chart_points.push(ChartPoint::new(bar_idx, price));
781 self.points.push(screen_point);
782 self.check_completion();
783 }
784
785 pub fn update_last_point_with_chart_coords<F, G>(
787 &mut self,
788 screen_point: egui::Pos2,
789 x_to_bar: F,
790 y_to_price: G,
791 ) where
792 F: Fn(f32) -> f32,
793 G: Fn(f32) -> f64,
794 {
795 if let (Some(last_point), Some(last_chart_point)) =
796 (self.points.last_mut(), self.chart_points.last_mut())
797 {
798 *last_point = screen_point;
799 *last_chart_point =
800 ChartPoint::new(x_to_bar(screen_point.x), y_to_price(screen_point.y));
801 }
802 }
803
804 pub(crate) fn align_second_anchor(&mut self) {
807 if self.points.len() < 2 || self.chart_points.len() < 2 {
808 return;
809 }
810 match self.tool_type {
811 DrawingToolType::TimeCycles => {
812 self.points[1].y = self.points[0].y;
813 self.chart_points[1].price = self.chart_points[0].price;
814 }
815 DrawingToolType::Signpost => {
816 self.points[1].x = self.points[0].x;
817 self.chart_points[1].bar_idx = self.chart_points[0].bar_idx;
818 }
819 _ => {}
820 }
821 }
822
823 pub fn add_point(&mut self, point: egui::Pos2) {
825 self.points.push(point);
826 self.check_completion();
827 }
828
829 pub fn shift_bar_indices(&mut self, from: f32, delta: f32) {
832 for cp in &mut self.chart_points {
833 if cp.bar_idx >= from {
834 cp.shift_bar_idx(delta);
835 }
836 }
837 }
838
839 pub(crate) fn first_moved_anchor(&self) -> usize {
843 if self.tool_type == DrawingToolType::BarsPattern && self.chart_points.len() >= 3 {
844 2
845 } else {
846 0
847 }
848 }
849
850 fn check_completion(&mut self) {
852 if let Some(required) = self.tool_type.required_points()
853 && self.points.len() >= required
854 {
855 self.completed = true;
856 }
857 }
858
859 #[inline]
861 pub fn color32(&self) -> egui::Color32 {
862 egui::Color32::from_rgba_unmultiplied(
863 self.color[0],
864 self.color[1],
865 self.color[2],
866 self.color[3],
867 )
868 }
869
870 #[inline]
872 pub fn fill_color32(&self) -> Option<egui::Color32> {
873 self.fill_color
874 .map(|c| egui::Color32::from_rgba_unmultiplied(c[0], c[1], c[2], c[3]))
875 }
876
877 #[inline]
879 pub fn stroke(&self) -> egui::Stroke {
880 egui::Stroke::new(self.stroke_width, self.color32())
881 }
882
883 #[inline]
885 pub fn is_visible_on_timeframe(&self, timeframe: &str) -> bool {
886 self.visible && self.timeframe_visibility.is_visible_on(timeframe)
887 }
888
889 pub(crate) fn axis_labels_of(tool: DrawingToolType) -> (bool, bool) {
893 use DrawingToolType as T;
894 match tool {
895 T::HorizontalLine | T::HorizontalRay => (true, false),
896 T::VerticalLine => (false, true),
897 T::CrossLine => (true, true),
898 _ => (false, false),
899 }
900 }
901
902 #[inline]
904 pub fn z_order(&self) -> i32 {
905 self.z_order
906 }
907
908 #[inline]
910 pub fn set_z_order(&mut self, order: i32) {
911 self.z_order = order;
912 }
913
914 pub fn bounding_rect(&self) -> Option<egui::Rect> {
916 if self.points.is_empty() {
917 return None;
918 }
919
920 let mut min_x = f32::MAX;
921 let mut min_y = f32::MAX;
922 let mut max_x = f32::MIN;
923 let mut max_y = f32::MIN;
924
925 for p in &self.points {
926 min_x = min_x.min(p.x);
927 min_y = min_y.min(p.y);
928 max_x = max_x.max(p.x);
929 max_y = max_y.max(p.y);
930 }
931
932 Some(egui::Rect::from_min_max(
933 egui::pos2(min_x, min_y),
934 egui::pos2(max_x, max_y),
935 ))
936 }
937
938 pub fn quadratic_bezier(p0: egui::Pos2, p1: egui::Pos2, p2: egui::Pos2, t: f32) -> egui::Pos2 {
943 let u = 1.0 - t;
944 egui::Pos2::new(
945 u * u * p0.x + 2.0 * u * t * p1.x + t * t * p2.x,
946 u * u * p0.y + 2.0 * u * t * p1.y + t * t * p2.y,
947 )
948 }
949}
950
951impl Default for Drawing {
952 fn default() -> Self {
953 Self::new(0, DrawingToolType::TrendLine)
954 }
955}
956
957impl Serialize for Drawing {
958 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
959 Self::serialize(self, serializer)
960 }
961}
962
963impl<'de> Deserialize<'de> for Drawing {
964 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
967 GannConfig::forget_left_out();
968 let mut drawing = Self::deserialize(deserializer)?;
969 if let Some(gann) = &mut drawing.gann {
970 gann.fill_left_out(drawing.tool_type);
971 }
972 Ok(drawing)
973 }
974}
975
976#[cfg(test)]
977mod tests {
978 use super::*;
979
980 #[test]
981 fn test_shift_bar_indices() {
982 let mut drawing = Drawing::new(1, DrawingToolType::TrendLine);
983 drawing.chart_points.push(ChartPoint::new(100.0, 150.0));
984 drawing.chart_points.push(ChartPoint::new(110.0, 160.0));
985
986 drawing.shift_bar_indices(105.0, 50.0);
987
988 assert_eq!(
989 drawing.chart_points,
990 [ChartPoint::new(100.0, 150.0), ChartPoint::new(160.0, 160.0)]
991 );
992 }
993
994 #[test]
998 fn test_completion() {
999 let x_to_bar = |x: f32| x / 10.0;
1000 let y_to_price = |y: f32| 1000.0 - y as f64;
1001 let mut drawing = Drawing::new(1, DrawingToolType::TrendLine);
1002
1003 drawing.add_point_with_chart_coords(egui::pos2(100.0, 300.0), x_to_bar, y_to_price);
1004 assert!(!drawing.completed);
1005
1006 drawing.add_point_with_chart_coords(egui::pos2(200.0, 50.0), x_to_bar, y_to_price);
1007 assert!(drawing.completed); assert_eq!(
1009 drawing.chart_points,
1010 [ChartPoint::new(10.0, 700.0), ChartPoint::new(20.0, 950.0)]
1011 );
1012 }
1013
1014 #[test]
1017 fn alert_levels_follow_the_drawing_geometry() {
1018 use DrawingToolType as T;
1019 use PriceScaleMode::{Logarithmic, Normal as Linear};
1020 let drawing = |tool, points: &[(f32, f64)]| {
1021 let mut drawing = Drawing::new(7, tool);
1022 drawing.chart_points = points.iter().map(|&(b, p)| ChartPoint::new(b, p)).collect();
1023 drawing
1024 };
1025 let mut fib = drawing(T::FibonacciRetracement, &[(0.0, 100.0), (10.0, 200.0)]);
1026 let mut config = FibonacciConfig {
1027 levels_as_percent: true,
1028 ..Default::default()
1029 };
1030 config.levels[1].visible = false;
1031 config.levels[2].label = "100%".into();
1032 config.levels[3].label.clear();
1033 fib.fib_config = Some(config);
1034 for (drawing, bar, scale, expected) in [
1036 (
1038 drawing(T::TrendLine, &[(10.0, 100.0), (20.0, 120.0)]),
1039 25.0,
1040 Linear,
1041 vec![(0, "Line", 130.0)],
1042 ),
1043 (
1044 drawing(T::TrendLine, &[(10.0, 100.0), (10.0, 120.0)]),
1045 25.0,
1046 Linear,
1047 vec![],
1048 ),
1049 (
1050 drawing(T::HorizontalRay, &[(3.0, 50.0)]),
1051 9.0,
1052 Linear,
1053 vec![(0, "Line", 50.0)],
1054 ),
1055 (
1056 drawing(T::Rect, &[(5.0, 90.0), (8.0, 110.0)]),
1057 9.0,
1058 Linear,
1059 vec![(0, "Top", 110.0), (1, "Bottom", 90.0)],
1060 ),
1061 (
1062 drawing(T::LongPos, &[(5.0, 100.0), (9.0, 110.0)]),
1063 9.0,
1064 Linear,
1065 vec![(0, "Entry", 100.0), (1, "Target", 110.0), (2, "Stop", 90.0)],
1066 ),
1067 (
1068 drawing(T::ShortPos, &[(5.0, 100.0), (9.0, 90.0), (9.0, 104.0)]),
1069 9.0,
1070 Linear,
1071 vec![(0, "Entry", 100.0), (1, "Target", 90.0), (2, "Stop", 104.0)],
1072 ),
1073 (
1075 fib,
1076 9.0,
1077 Linear,
1078 vec![
1079 (0, "0%", 200.0),
1080 (2, "38.2%", 161.8),
1081 (3, "50%", 150.0),
1082 (4, "61.8%", 138.2),
1083 (5, "78.6%", 121.4),
1084 (6, "100%", 100.0),
1085 ],
1086 ),
1087 (
1089 drawing(T::TrendLine, &[(10.0, 100.0), (20.0, 400.0)]),
1090 15.0,
1091 Logarithmic,
1092 vec![(0, "Line", 200.0)],
1093 ),
1094 (
1095 drawing(T::TrendLine, &[(10.0, 100.0), (20.0, 400.0)]),
1096 15.0,
1097 Linear,
1098 vec![(0, "Line", 250.0)],
1099 ),
1100 (
1101 drawing(T::TrendLine, &[(10.0, 0.0), (20.0, 10.0)]),
1102 15.0,
1103 Logarithmic,
1104 vec![],
1105 ),
1106 (
1110 drawing(
1111 T::ParallelChannel,
1112 &[(10.0, 100.0), (20.0, 120.0), (15.0, 130.0)],
1113 ),
1114 25.0,
1115 Linear,
1116 vec![(0, "Line", 130.0), (1, "Parallel line", 150.0)],
1117 ),
1118 (
1119 drawing(
1120 T::ParallelChannel,
1121 &[(10.0, 100.0), (20.0, 400.0), (15.0, 400.0)],
1122 ),
1123 20.0,
1124 Logarithmic,
1125 vec![(0, "Line", 400.0), (1, "Parallel line", 800.0)],
1126 ),
1127 (
1128 drawing(
1129 T::ParallelChannel,
1130 &[(10.0, 100.0), (10.0, 120.0), (15.0, 130.0)],
1131 ),
1132 25.0,
1133 Linear,
1134 vec![],
1135 ),
1136 (
1137 drawing(
1138 T::FlatTopBottom,
1139 &[(10.0, 100.0), (20.0, 120.0), (15.0, 90.0)],
1140 ),
1141 25.0,
1142 Linear,
1143 vec![(0, "Line", 130.0), (1, "Flat line", 90.0)],
1144 ),
1145 (
1148 drawing(
1149 T::DisjointChannel,
1150 &[(10.0, 100.0), (20.0, 120.0), (12.0, 50.0)],
1151 ),
1152 25.0,
1153 Linear,
1154 vec![(0, "Line", 130.0), (1, "Second line", 24.0)],
1155 ),
1156 (
1157 drawing(
1158 T::DisjointChannel,
1159 &[(10.0, 100.0), (20.0, 400.0), (12.0, 50.0)],
1160 ),
1161 22.0,
1162 Logarithmic,
1163 vec![
1164 (0, "Line", 100.0 * 4f64.powf(1.2)),
1165 (1, "Second line", 12.5),
1166 ],
1167 ),
1168 (
1169 drawing(T::Brush, &[(1.0, 1.0), (2.0, 2.0)]),
1170 9.0,
1171 Linear,
1172 vec![],
1173 ),
1174 ] {
1175 let levels = drawing.alert_levels(bar, scale);
1176 assert_eq!(levels.len(), expected.len(), "{:?}", drawing.tool_type);
1177 for ((level, name, price), (want_level, want_name, want_price)) in
1178 levels.into_iter().zip(expected)
1179 {
1180 assert_eq!((level, name.as_str()), (want_level, want_name));
1181 assert!((price - want_price).abs() < 1e-4, "{name}: {price}");
1183 }
1184 }
1185
1186 let mut line = drawing(T::TrendLine, &[(10.0, 100.0), (20.0, 120.0)]);
1187 for (stored, level, source) in [
1188 (None, 0, "7"),
1189 (None, 2, "7#2"),
1190 (Some("a%231"), 0, "a%25231"),
1191 (Some("a#1"), 3, "a%231#3"),
1192 ] {
1193 line.stored_id = stored.map(Into::into);
1194 assert_eq!(
1195 line.alert_source(level),
1196 AlertSource::Drawing(source.into())
1197 );
1198 assert_eq!(line.alert_level(&line.alert_source(level)), Some(level));
1199 }
1200 for other in ["a", "a#1#0", "a#1#x", "7"] {
1201 assert_eq!(line.alert_level(&AlertSource::Drawing(other.into())), None);
1202 }
1203 assert_eq!(line.alert_level(&AlertSource::Price), None);
1204
1205 let mut other = line.clone();
1207 other.stored_id = Some("a".into());
1208 assert_ne!(line.alert_source(0), other.alert_source(1));
1209 }
1210
1211 #[test]
1212 fn test_bounding_rect() {
1213 let mut drawing = Drawing::new(1, DrawingToolType::TrendLine);
1214 assert!(drawing.bounding_rect().is_none());
1215
1216 drawing.points.push(egui::pos2(10.0, 20.0));
1217 drawing.points.push(egui::pos2(30.0, 50.0));
1218
1219 let rect = drawing.bounding_rect().unwrap();
1220 assert_eq!(rect.min, egui::pos2(10.0, 20.0));
1221 assert_eq!(rect.max, egui::pos2(30.0, 50.0));
1222 }
1223}