1use super::indicators::{IndicatorAttachmentMode, IndicatorPanelRecipe};
2use super::{IndicatorId, IndicatorInstanceId, IndicatorInstanceStore, IndicatorOutputRef};
3use super::{YUnit, YUnitRange};
4pub use crate::chart::value::{PanelValueLabelMode, PanelValueLabelPolicy, PanelValuePrecision};
5use crate::util::stable_hash;
6use exchange::{Ticker, TickerInfo};
7use indicator_engine::PaneTarget;
8use rustc_hash::FxHashSet;
9use serde::{Deserialize, Serialize};
10use std::cmp::Ordering;
11
12pub const DEFAULT_MIN_PANEL_RATIO: f32 = 0.08;
13const DEFAULT_AUXILIARY_PANEL_RATIO: f32 = 0.15;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
16pub enum PanelId {
17 Main,
18 Aux(uuid::Uuid),
19}
20
21impl PanelId {
22 pub(crate) fn named(pane: u32) -> Self {
23 Self::Aux(uuid::Uuid::from_u64_pair(
24 stable_hash(b"panel.named"),
25 u64::from(pane),
26 ))
27 }
28
29 pub(crate) fn indicator(
30 indicator: &IndicatorId,
31 presentation: PanelPresentation,
32 slot: u32,
33 ) -> Self {
34 let presentation = match presentation {
35 PanelPresentation::Plot => 0_u64,
36 PanelPresentation::Table => 1,
37 };
38
39 Self::Aux(uuid::Uuid::from_u64_pair(
40 stable_hash(indicator.id().as_bytes()),
41 (presentation << 32) | u64::from(slot),
42 ))
43 }
44
45 pub fn is_main(self) -> bool {
46 matches!(self, Self::Main)
47 }
48
49 pub fn is_aux(self) -> bool {
50 !self.is_main()
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
55pub struct LayerId(pub u32);
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
58pub enum PanelPresentation {
59 #[default]
60 Plot,
61 Table,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
65pub enum LayerPurpose {
66 #[default]
67 Plot,
68 TableBinding,
70}
71
72impl LayerPurpose {
73 pub const fn is_plot(self) -> bool {
74 matches!(self, Self::Plot)
75 }
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
79pub enum MarkKind {
80 Candle,
81 Bar(BarMode),
82 Line,
83 StepLine,
84 Area,
85 Point,
86 Cross,
87 Segment,
88 HorizontalLevel,
89}
90
91impl MarkKind {
92 pub const fn supports_line_styling(self) -> bool {
93 self.is_line_like()
94 }
95
96 pub const fn is_line_like(self) -> bool {
97 matches!(
98 self,
99 Self::Line | Self::StepLine | Self::Area | Self::Segment | Self::HorizontalLevel
100 )
101 }
102
103 pub const fn is_series_line_like(self) -> bool {
106 self.is_line_like()
107 }
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
111pub enum LayerDataKind {
112 Ohlc,
113 Scalar,
114 Histogram,
115 Range,
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
119pub enum HistogramMode {
120 Plain,
121 SignedOverlay,
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
125pub enum BarMode {
126 Regular,
127 Histogram(HistogramMode),
128}
129
130const fn default_bar_mode_for_data_kind(data_kind: LayerDataKind) -> BarMode {
131 match data_kind {
132 LayerDataKind::Histogram => BarMode::Histogram(HistogramMode::Plain),
133 LayerDataKind::Ohlc | LayerDataKind::Scalar | LayerDataKind::Range => BarMode::Regular,
134 }
135}
136
137pub const fn default_mark_for_data_kind(data_kind: LayerDataKind) -> MarkKind {
138 match data_kind {
139 LayerDataKind::Ohlc => MarkKind::Candle,
140 LayerDataKind::Scalar | LayerDataKind::Range => MarkKind::Line,
141 LayerDataKind::Histogram => MarkKind::Bar(BarMode::Histogram(HistogramMode::Plain)),
142 }
143}
144
145const fn supports_mark(data_kind: LayerDataKind, mark: MarkKind) -> bool {
146 match data_kind {
147 LayerDataKind::Ohlc => matches!(mark, MarkKind::Line | MarkKind::Candle),
148 LayerDataKind::Scalar => matches!(
149 mark,
150 MarkKind::Line
151 | MarkKind::StepLine
152 | MarkKind::Area
153 | MarkKind::Point
154 | MarkKind::Cross
155 | MarkKind::Segment
156 | MarkKind::HorizontalLevel
157 | MarkKind::Bar(_)
158 ),
159 LayerDataKind::Range => matches!(mark, MarkKind::Line),
160 LayerDataKind::Histogram => matches!(
161 mark,
162 MarkKind::Line
163 | MarkKind::StepLine
164 | MarkKind::Area
165 | MarkKind::Point
166 | MarkKind::Cross
167 | MarkKind::Segment
168 | MarkKind::HorizontalLevel
169 | MarkKind::Bar(_)
170 ),
171 }
172}
173
174const fn supports_bar_mode(data_kind: LayerDataKind, mode: BarMode) -> bool {
175 match mode {
176 BarMode::Regular => true,
177 BarMode::Histogram(HistogramMode::Plain) => matches!(data_kind, LayerDataKind::Histogram),
178 BarMode::Histogram(HistogramMode::SignedOverlay) => {
179 matches!(data_kind, LayerDataKind::Histogram)
180 }
181 }
182}
183
184pub fn resolve_mark_for_data_kind(mark: MarkKind, data_kind: LayerDataKind) -> MarkKind {
185 let mut resolved = if supports_mark(data_kind, mark) {
186 mark
187 } else {
188 default_mark_for_data_kind(data_kind)
189 };
190
191 if let MarkKind::Bar(mode) = resolved
192 && !supports_bar_mode(data_kind, mode)
193 {
194 resolved = MarkKind::Bar(default_bar_mode_for_data_kind(data_kind));
195 }
196
197 resolved
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
201pub enum AxisBinding {
202 Primary,
203 Secondary,
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
207pub enum DataSourceId {
208 Primary,
209 Comparison(Ticker),
210}
211
212#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
213pub enum PanelScaleMode {
214 Absolute,
215 FitVisible,
216 FitVisibleIncludeZero,
217 Logarithmic,
218 PercentFromBase,
219}
220
221impl PanelScaleMode {
222 pub fn uses_percent_base(self) -> bool {
223 matches!(self, Self::PercentFromBase)
224 }
225
226 pub fn uses_series_percent_anchors(self, series_count: usize) -> bool {
227 self.uses_percent_base() && series_count > 1
228 }
229
230 pub fn primary_autoscale_enabled(self, user_autoscale_enabled: bool) -> bool {
231 user_autoscale_enabled || matches!(self, Self::PercentFromBase)
232 }
233
234 pub fn allows_primary_manual_pan(self, user_autoscale_enabled: bool) -> bool {
235 !self.primary_autoscale_enabled(user_autoscale_enabled)
236 }
237
238 pub fn allows_primary_manual_zoom(self) -> bool {
239 !matches!(self, Self::PercentFromBase)
240 }
241
242 pub fn display_anchor(
243 self,
244 explicit_anchor: Option<f32>,
245 default_anchor: Option<f32>,
246 ) -> Option<f32> {
247 if self.uses_percent_base() {
248 explicit_anchor
249 } else {
250 default_anchor
251 }
252 }
253
254 pub fn format_percent_label(
255 self,
256 display_value: f32,
257 display_step: f32,
258 show_decimals: bool,
259 ) -> Option<String> {
260 if !self.uses_percent_base() {
261 return None;
262 }
263
264 let format_pct = |value: f32, step: f32, show_decimals: bool| {
265 let precision = if show_decimals || step.fract() != 0.0 {
266 if step >= 1.0 {
267 1
268 } else if step >= 0.1 {
269 2
270 } else {
271 3
272 }
273 } else if step >= 1.0 {
274 0
275 } else if step >= 0.1 {
276 1
277 } else {
278 2
279 };
280
281 format!("{value:+.*}%", precision)
282 };
283
284 Some(format_pct(display_value, display_step, show_decimals))
285 }
286
287 pub fn log_axis_value_and_step(
288 self,
289 display_value: f32,
290 display_step: f32,
291 anchor: Option<f32>,
292 log_scale_enabled: bool,
293 ) -> Option<(f32, f32)> {
294 if !matches!(self, Self::Logarithmic) || !log_scale_enabled {
295 return None;
296 }
297
298 let value = self.display_to_value(display_value, anchor, true);
299 let next_value =
300 self.display_to_value(display_value + display_step.abs().max(1e-3), anchor, true);
301 let value_step = (next_value - value).abs().max(1e-6);
302
303 Some((value, value_step))
304 }
305
306 pub fn uses_display_transform(self, log_scale_enabled: bool) -> bool {
307 matches!(self, Self::PercentFromBase)
308 || (log_scale_enabled && matches!(self, Self::Logarithmic))
309 }
310
311 pub fn value_to_display(self, value: f32, anchor: Option<f32>, log_scale_enabled: bool) -> f32 {
312 if log_scale_enabled && matches!(self, Self::Logarithmic) {
313 value.max(f32::MIN_POSITIVE).log10()
314 } else {
315 match (self, anchor) {
316 (Self::PercentFromBase, Some(base)) if base.abs() > f32::EPSILON => {
317 ((value / base) - 1.0) * 100.0
318 }
319 _ => value,
320 }
321 }
322 }
323
324 pub fn display_to_value(
325 self,
326 display_value: f32,
327 anchor: Option<f32>,
328 log_scale_enabled: bool,
329 ) -> f32 {
330 if log_scale_enabled && matches!(self, Self::Logarithmic) {
331 10_f32.powf(display_value).max(f32::MIN_POSITIVE)
332 } else {
333 match (self, anchor) {
334 (Self::PercentFromBase, Some(base)) if base.abs() > f32::EPSILON => {
335 base * (1.0 + display_value / 100.0)
336 }
337 _ => display_value,
338 }
339 }
340 }
341
342 pub fn indicator_domain_units(self, min_unit: i64, max_unit: i64) -> (YUnit, YUnit) {
343 let range = match self {
344 Self::FitVisible => YUnitRange::new(YUnit(min_unit), YUnit(max_unit)).padded(),
345 Self::FitVisibleIncludeZero => {
346 YUnitRange::new(YUnit(min_unit.min(0)), YUnit(max_unit.max(0))).padded()
347 }
348 _ => YUnitRange::new(YUnit(0), YUnit(max_unit.max(1))),
349 };
350 (range.min(), range.max())
351 }
352}
353
354pub type PanelValueId = super::IndicatorInstanceId;
355
356#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
357pub struct PanelComparisonPolicy {
358 pub force_percent_scale_on_multi_source: bool,
359 pub force_line_for_non_base_sources: bool,
360}
361
362impl Default for PanelComparisonPolicy {
363 fn default() -> Self {
364 Self {
365 force_percent_scale_on_multi_source: true,
366 force_line_for_non_base_sources: true,
367 }
368 }
369}
370
371#[derive(Debug, Clone, PartialEq, Eq)]
372pub enum LayerSource {
373 RawKline {
374 source: DataSourceId,
375 },
376 IndicatorOutput {
377 source: DataSourceId,
378 output: IndicatorOutputRef,
379 },
380}
381
382#[derive(Debug, Clone, Copy, PartialEq, Eq)]
383pub enum PanelDataHint {
384 ValueLike,
385 HistogramLike,
386 TableLike,
387}
388
389#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
390pub struct LayerStyle {
391 pub line_width: f32,
392 pub opacity: f32,
393}
394
395impl Default for LayerStyle {
396 fn default() -> Self {
397 Self {
398 line_width: 1.0,
399 opacity: 1.0,
400 }
401 }
402}
403
404#[derive(Debug, Clone, PartialEq)]
405pub struct LayerSpec {
406 pub id: LayerId,
407 pub name: String,
408 pub source: LayerSource,
409 pub data_kind: LayerDataKind,
410 pub purpose: LayerPurpose,
411 pub mark: MarkKind,
412 pub mark_override: Option<MarkKind>,
413 pub axis: AxisBinding,
414 pub visible: bool,
415 pub style: LayerStyle,
416}
417
418impl LayerSpec {
419 pub fn effective_mark(&self) -> MarkKind {
420 self.mark_override.unwrap_or(self.mark)
421 }
422
423 fn is_histogram_like(&self) -> bool {
424 matches!(self.axis, AxisBinding::Secondary)
425 || matches!(self.data_kind, LayerDataKind::Histogram)
426 }
427
428 pub fn source_id(&self) -> Option<DataSourceId> {
429 self.source.source_id()
430 }
431}
432
433impl LayerSource {
434 pub fn source_id(&self) -> Option<DataSourceId> {
435 match self {
436 Self::RawKline { source } => Some(*source),
437 Self::IndicatorOutput { source, .. } => Some(*source),
438 }
439 }
440
441 pub fn indicator_output(&self) -> Option<&IndicatorOutputRef> {
442 match self {
443 Self::IndicatorOutput { output, .. } => Some(output),
444 _ => None,
445 }
446 }
447
448 fn references_instance(&self, id: IndicatorInstanceId) -> bool {
449 self.indicator_output()
450 .is_some_and(|output| output.instance_id == id)
451 }
452}
453
454#[derive(Debug, Clone, PartialEq)]
455pub struct PanelSpec {
456 pub id: PanelId,
457 pub presentation: PanelPresentation,
458 pub named_pane: Option<u32>,
459 pub title: Option<String>,
460 pub value_precision: Option<PanelValuePrecision>,
461 pub value_label_policy: PanelValueLabelPolicy,
462 pub base_layer: Option<LayerId>,
463 pub preferred_scale: PanelScaleMode,
464 pub comparison_policy: PanelComparisonPolicy,
465 pub layers: Vec<LayerSpec>,
466}
467
468impl PanelSpec {
469 pub fn effective_data_kind(&self) -> LayerDataKind {
470 let fallback = if self.id.is_main() {
471 LayerDataKind::Ohlc
472 } else {
473 LayerDataKind::Scalar
474 };
475
476 self.base_layer
477 .and_then(|base| self.layers.iter().find(|layer| layer.id == base))
478 .or_else(|| self.layers.first())
479 .map(|layer| layer.data_kind)
480 .unwrap_or(fallback)
481 }
482
483 pub fn effective_mark(&self) -> MarkKind {
484 let fallback = if self.id.is_main() {
485 default_mark_for_data_kind(LayerDataKind::Ohlc)
486 } else {
487 MarkKind::Bar(BarMode::Histogram(HistogramMode::Plain))
488 };
489
490 let base_layer_id = self
491 .base_layer
492 .or_else(|| self.layers.first().map(|layer| layer.id));
493
494 let resolved_marks = self.resolved_marks();
495
496 base_layer_id
497 .and_then(|base| {
498 resolved_marks
499 .iter()
500 .find(|(layer_id, _)| *layer_id == base)
501 .map(|(_, mark)| *mark)
502 })
503 .or_else(|| resolved_marks.first().map(|(_, mark)| *mark))
504 .unwrap_or(fallback)
505 }
506
507 pub fn resolved_marks(&self) -> Vec<(LayerId, MarkKind)> {
508 let is_multi_source = self.uses_multi_source();
509
510 self.layers
511 .iter()
512 .map(|layer| {
513 let mut mark = layer.effective_mark();
514
515 if self.comparison_policy.force_line_for_non_base_sources
516 && is_multi_source
517 && self.base_layer != Some(layer.id)
518 {
519 mark = MarkKind::Line;
520 }
521
522 (layer.id, resolve_mark_for_data_kind(mark, layer.data_kind))
523 })
524 .collect()
525 }
526
527 fn add_layer(&mut self, layer: LayerSpec) -> bool {
528 let layer_id = layer.id;
529 self.layers.push(layer);
530 if self.base_layer.is_none() {
531 self.base_layer = Some(layer_id);
532 }
533
534 true
535 }
536
537 fn remove_layer(&mut self, layer_id: LayerId) -> bool {
538 let Some(index) = self.layers.iter().position(|layer| layer.id == layer_id) else {
539 return false;
540 };
541
542 self.layers.remove(index);
543
544 if self.base_layer == Some(layer_id) {
545 self.base_layer = self.layers.first().map(|layer| layer.id);
546 }
547
548 true
549 }
550
551 pub fn indicator_visible(&self, id: PanelValueId) -> bool {
552 self.layers.iter().any(|layer| {
553 layer.visible
554 && (!matches!(self.presentation, PanelPresentation::Table)
555 || layer.purpose == LayerPurpose::TableBinding)
556 && layer.source.references_instance(id)
557 })
558 }
559
560 fn data_hint(&self) -> PanelDataHint {
561 if matches!(self.presentation, PanelPresentation::Table) {
562 return PanelDataHint::TableLike;
563 }
564
565 if self.layers.iter().any(LayerSpec::is_histogram_like) {
566 PanelDataHint::HistogramLike
567 } else {
568 PanelDataHint::ValueLike
569 }
570 }
571
572 fn source_count(&self) -> usize {
573 self.layers
574 .iter()
575 .filter_map(LayerSpec::source_id)
576 .collect::<FxHashSet<DataSourceId>>()
577 .len()
578 }
579
580 pub fn uses_multi_source(&self) -> bool {
581 self.source_count() > 1
582 }
583
584 fn effective_scale_mode(&self) -> PanelScaleMode {
585 if self.comparison_policy.force_percent_scale_on_multi_source && self.uses_multi_source() {
586 PanelScaleMode::PercentFromBase
587 } else {
588 self.preferred_scale
589 }
590 }
591
592 fn set_layer_mark(&mut self, layer_id: LayerId, mark: MarkKind) -> bool {
593 let Some(layer) = self.layers.iter_mut().find(|layer| layer.id == layer_id) else {
594 return false;
595 };
596
597 layer.mark_override = Some(mark);
598 true
599 }
600}
601
602#[derive(Debug, Clone)]
612pub struct ChartComposition {
613 pub panels: Vec<PanelSpec>,
614 pub splits: Vec<f32>,
616 next_layer_id: u32,
617}
618
619impl ChartComposition {
620 pub fn comparison_layer(&self, ticker: TickerInfo) -> Option<&LayerSpec> {
621 let panel = self.panel(self.primary_panel_id()?)?;
622 panel.layers.iter().find(|layer| {
623 matches!(
624 layer.source,
625 LayerSource::RawKline {
626 source: DataSourceId::Comparison(source)
627 } if source == ticker.ticker
628 )
629 })
630 }
631
632 pub fn toggle_comparison_visibility(&mut self, ticker: TickerInfo) -> bool {
633 let Some(panel_id) = self.primary_panel_id() else {
634 return false;
635 };
636 let Some(layer) = self.comparison_layer(ticker) else {
637 return false;
638 };
639 let (layer_id, visible) = (layer.id, !layer.visible);
640 self.set_panel_layer_visibility(panel_id, layer_id, visible)
641 }
642
643 pub fn prototype_kline() -> Self {
644 let mut composition = Self {
645 panels: Vec::new(),
646 splits: Vec::new(),
647 next_layer_id: 1,
648 };
649
650 let candle_layer = composition.new_layer(
651 "Candles",
652 LayerSource::RawKline {
653 source: DataSourceId::Primary,
654 },
655 LayerDataKind::Ohlc,
656 MarkKind::Candle,
657 AxisBinding::Primary,
658 );
659
660 composition.panels.push(PanelSpec {
661 id: PanelId::Main,
662 presentation: PanelPresentation::Plot,
663 named_pane: None,
664 title: None,
665 value_precision: Some(PanelValuePrecision::BaseTickerMinTick),
666 value_label_policy: PanelValueLabelPolicy::default(),
667 base_layer: Some(candle_layer.id),
668 preferred_scale: PanelScaleMode::Absolute,
669 comparison_policy: PanelComparisonPolicy::default(),
670 layers: vec![candle_layer],
671 });
672
673 composition.ensure_split_count();
674 composition.splits = composition.normalized_splits(DEFAULT_MIN_PANEL_RATIO);
675 composition
676 }
677
678 pub fn from_parts(panels: Vec<PanelSpec>, splits: Vec<f32>) -> Option<Self> {
679 if panels.iter().filter(|panel| panel.id.is_main()).count() != 1 {
680 return None;
681 }
682
683 let mut ids = FxHashSet::default();
684 if panels.iter().any(|panel| !ids.insert(panel.id)) {
685 return None;
686 }
687
688 let next_layer_id = panels
689 .iter()
690 .flat_map(|panel| panel.layers.iter())
691 .map(|layer| layer.id.0)
692 .max()
693 .unwrap_or(0)
694 .saturating_add(1);
695
696 let mut composition = Self {
697 panels,
698 splits,
699 next_layer_id,
700 };
701 composition.ensure_split_count();
702 composition.splits = composition.normalized_splits(DEFAULT_MIN_PANEL_RATIO);
703 Some(composition)
704 }
705
706 pub fn panel_count(&self) -> usize {
707 self.panels.len()
708 }
709
710 pub fn primary_panel_id(&self) -> Option<PanelId> {
711 self.panels
712 .iter()
713 .find(|panel| panel.id.is_main())
714 .map(|panel| panel.id)
715 }
716
717 pub fn split_count(&self) -> usize {
718 self.panels.len().saturating_sub(1)
719 }
720
721 pub fn panel(&self, panel_id: PanelId) -> Option<&PanelSpec> {
722 self.panels.iter().find(|panel| panel.id == panel_id)
723 }
724
725 fn panel_mut(&mut self, panel_id: PanelId) -> Option<&mut PanelSpec> {
726 self.panels.iter_mut().find(|panel| panel.id == panel_id)
727 }
728
729 pub fn panel_effective_scale_mode(&self, panel_id: PanelId) -> Option<PanelScaleMode> {
730 self.panel(panel_id).map(PanelSpec::effective_scale_mode)
731 }
732
733 pub fn panel_effective_mark(&self, panel_id: PanelId) -> Option<MarkKind> {
734 self.panel(panel_id).map(PanelSpec::effective_mark)
735 }
736
737 pub fn resolved_panel_marks(&self, panel_id: PanelId) -> Option<Vec<(LayerId, MarkKind)>> {
738 self.panel(panel_id).map(PanelSpec::resolved_marks)
739 }
740
741 pub fn panel_value_precision(&self, panel_id: PanelId) -> Option<PanelValuePrecision> {
742 self.panel(panel_id).and_then(|panel| panel.value_precision)
743 }
744
745 pub fn panel_value_label_policy(&self, panel_id: PanelId) -> Option<PanelValueLabelPolicy> {
746 self.panel(panel_id).map(|panel| panel.value_label_policy)
747 }
748
749 pub fn normalized_splits(&self, min_panel_ratio: f32) -> Vec<f32> {
750 let panel_count = self.panel_count();
751 let split_count = panel_count.saturating_sub(1);
752
753 if split_count == 0 {
754 return Vec::new();
755 }
756
757 let min_ratio = if panel_count == 0 {
758 0.0
759 } else {
760 min_panel_ratio.clamp(0.0, 1.0 / panel_count as f32)
761 };
762
763 let mut splits = Vec::with_capacity(split_count);
764 for index in 0..split_count {
765 let fallback = (index + 1) as f32 / panel_count as f32;
766 splits.push(self.splits.get(index).copied().unwrap_or(fallback));
767 }
768
769 for index in 0..split_count {
770 let remaining_panels_after = panel_count.saturating_sub(index + 1);
771 let lower = if index > 0 {
772 splits[index - 1] + min_ratio
773 } else {
774 min_ratio
775 };
776 let upper = 1.0 - (remaining_panels_after as f32 * min_ratio);
777
778 let (min_bound, max_bound) = if lower <= upper {
779 (lower, upper)
780 } else {
781 (upper, lower)
782 };
783
784 splits[index] = splits[index].clamp(min_bound, max_bound);
785 }
786
787 splits
788 }
789
790 pub fn set_split(&mut self, split_index: usize, split: f32, min_panel_ratio: f32) -> bool {
791 if split_index >= self.split_count() {
792 return false;
793 }
794
795 self.ensure_split_count();
796 let mut splits = self.normalized_splits(min_panel_ratio);
797
798 let panel_count = self.panel_count();
799 let min_ratio = if panel_count == 0 {
800 0.0
801 } else {
802 min_panel_ratio.clamp(0.0, 1.0 / panel_count as f32)
803 };
804
805 let lower = if split_index > 0 {
806 splits[split_index - 1] + min_ratio
807 } else {
808 min_ratio
809 };
810
811 let upper = if split_index + 1 < splits.len() {
812 splits[split_index + 1] - min_ratio
813 } else {
814 1.0 - min_ratio
815 };
816
817 let (min_bound, max_bound) = if lower <= upper {
818 (lower, upper)
819 } else {
820 (upper, lower)
821 };
822
823 let new_value = split.clamp(min_bound, max_bound);
824 if let Some(target) = splits.get_mut(split_index) {
825 *target = new_value;
826 }
827
828 self.splits = splits;
829 true
830 }
831
832 fn next_indicator_panel_id(
833 &self,
834 indicator: &IndicatorId,
835 presentation: PanelPresentation,
836 ) -> PanelId {
837 let mut slot = 0;
838 while self
839 .panels
840 .iter()
841 .any(|panel| panel.id == PanelId::indicator(indicator, presentation, slot))
842 {
843 slot += 1;
844 }
845 PanelId::indicator(indicator, presentation, slot)
846 }
847
848 fn add_aux_panel(
851 &mut self,
852 panel_id: PanelId,
853 title: impl Into<String>,
854 layers: Vec<LayerSpec>,
855 value_precision: Option<PanelValuePrecision>,
856 value_label_policy: PanelValueLabelPolicy,
857 preferred_scale: PanelScaleMode,
858 presentation: PanelPresentation,
859 ) -> Option<PanelId> {
860 let mut heights = self.normalized_panel_heights(DEFAULT_MIN_PANEL_RATIO);
861 let primary_index = self.panels.iter().position(|panel| panel.id.is_main())?;
862 let available_height = heights[primary_index] - DEFAULT_MIN_PANEL_RATIO;
863 if available_height + f32::EPSILON < DEFAULT_MIN_PANEL_RATIO {
864 return None;
865 }
866 let preferred_height = self
867 .panels
868 .last()
869 .filter(|panel| panel.id.is_aux())
870 .and_then(|_| heights.last().copied())
871 .unwrap_or(DEFAULT_AUXILIARY_PANEL_RATIO);
872 let auxiliary_height = preferred_height.min(available_height.max(DEFAULT_MIN_PANEL_RATIO));
873 let base_layer = layers.first().map(|layer| layer.id);
874 self.panels.push(PanelSpec {
875 id: panel_id,
876 presentation,
877 named_pane: None,
878 title: Some(title.into()),
879 value_precision,
880 value_label_policy,
881 base_layer,
882 preferred_scale,
883 comparison_policy: PanelComparisonPolicy::default(),
884 layers,
885 });
886
887 heights[primary_index] -= auxiliary_height;
888 heights.push(auxiliary_height);
889 self.splits = Self::splits_from_heights(&heights);
890 self.splits = self.normalized_splits(DEFAULT_MIN_PANEL_RATIO);
891 Some(panel_id)
892 }
893
894 fn named_panel_id(&self, named_pane: u32) -> Option<PanelId> {
895 self.panels
896 .iter()
897 .find(|panel| {
898 panel.id.is_aux()
899 && panel.presentation == PanelPresentation::Plot
900 && panel.named_pane == Some(named_pane)
901 })
902 .map(|panel| panel.id)
903 }
904
905 fn add_named_panel(
906 &mut self,
907 named_pane: u32,
908 title: impl Into<String>,
909 value_precision: PanelValuePrecision,
910 value_label_policy: PanelValueLabelPolicy,
911 preferred_scale: PanelScaleMode,
912 ) -> Option<PanelId> {
913 let panel_id = self.add_aux_panel(
914 PanelId::named(named_pane),
915 title,
916 Vec::new(),
917 Some(value_precision),
918 value_label_policy,
919 preferred_scale,
920 PanelPresentation::Plot,
921 )?;
922 if let Some(panel) = self.panel_mut(panel_id) {
923 panel.named_pane = Some(named_pane);
924 }
925 Some(panel_id)
926 }
927
928 fn add_table_panel(
929 &mut self,
930 title: impl Into<String>,
931 source: DataSourceId,
932 indicator: &IndicatorId,
933 ) -> Option<PanelId> {
934 let title = title.into();
935 let panel_id = self.next_indicator_panel_id(indicator, PanelPresentation::Table);
936 let panel_id = self.add_aux_panel(
937 panel_id,
938 title.clone(),
939 Vec::new(),
940 None,
941 PanelValueLabelPolicy::default(),
942 PanelScaleMode::Absolute,
943 PanelPresentation::Table,
944 )?;
945 let mut layer = self.new_layer(
946 title,
947 LayerSource::RawKline { source },
948 LayerDataKind::Scalar,
949 MarkKind::Line,
950 AxisBinding::Secondary,
951 );
952 layer.purpose = LayerPurpose::TableBinding;
953
954 self.add_layer_to_panel(panel_id, layer);
955 Some(panel_id)
956 }
957
958 fn add_layer_to_panel(&mut self, panel_id: PanelId, layer: LayerSpec) -> bool {
959 self.panel_mut(panel_id)
960 .is_some_and(|panel| panel.add_layer(layer))
961 }
962
963 fn remove_layer_from_panel(&mut self, panel_id: PanelId, layer_id: LayerId) -> bool {
964 self.panel_mut(panel_id)
965 .is_some_and(|panel| panel.remove_layer(layer_id))
966 }
967
968 fn add_comparison_source_to_panel(
969 &mut self,
970 panel_id: PanelId,
971 source: DataSourceId,
972 name: impl Into<String>,
973 ) -> Option<LayerId> {
974 let data_hint = self.panel(panel_id)?.data_hint();
975
976 let (data_kind, axis) = match data_hint {
977 PanelDataHint::ValueLike => (LayerDataKind::Scalar, AxisBinding::Primary),
978 PanelDataHint::HistogramLike => (LayerDataKind::Histogram, AxisBinding::Secondary),
979 PanelDataHint::TableLike => return None,
980 };
981
982 let layer = self.new_layer(
983 name,
984 LayerSource::RawKline { source },
985 data_kind,
986 MarkKind::Line,
987 axis,
988 );
989
990 let layer_id = layer.id;
991 self.add_layer_to_panel(panel_id, layer).then_some(layer_id)
992 }
993
994 pub fn set_panel_preferred_scale(&mut self, panel_id: PanelId, scale: PanelScaleMode) -> bool {
995 let Some(panel) = self.panel_mut(panel_id) else {
996 return false;
997 };
998
999 panel.preferred_scale = scale;
1000 true
1001 }
1002
1003 fn set_panel_layer_mark(
1004 &mut self,
1005 panel_id: PanelId,
1006 layer_id: LayerId,
1007 mark: MarkKind,
1008 ) -> bool {
1009 let Some(panel) = self.panel_mut(panel_id) else {
1010 return false;
1011 };
1012
1013 panel.set_layer_mark(layer_id, mark)
1014 }
1015
1016 fn set_panel_layer_visibility(
1017 &mut self,
1018 panel_id: PanelId,
1019 layer_id: LayerId,
1020 visible: bool,
1021 ) -> bool {
1022 let Some(panel) = self.panel_mut(panel_id) else {
1023 return false;
1024 };
1025 let Some(layer) = panel.layers.iter_mut().find(|layer| layer.id == layer_id) else {
1026 return false;
1027 };
1028
1029 if layer.visible == visible {
1030 return false;
1031 }
1032
1033 layer.visible = visible;
1034 true
1035 }
1036
1037 fn clear_panel_layer_mark(&mut self, panel_id: PanelId, layer_id: LayerId) -> bool {
1038 let Some(panel) = self.panel_mut(panel_id) else {
1039 return false;
1040 };
1041 let Some(layer) = panel.layers.iter_mut().find(|layer| layer.id == layer_id) else {
1042 return false;
1043 };
1044 layer.mark_override.take().is_some()
1045 }
1046
1047 pub fn remove_panel(&mut self, panel_id: PanelId) -> bool {
1048 let Some(index) = self.panels.iter().position(|panel| panel.id == panel_id) else {
1049 return false;
1050 };
1051
1052 if self.panels[index].id.is_main() {
1053 return false;
1054 }
1055
1056 let mut heights = self.normalized_panel_heights(DEFAULT_MIN_PANEL_RATIO);
1057 let removed_height = heights.remove(index);
1058 self.panels.remove(index);
1059 if let Some(primary_index) = self.panels.iter().position(|panel| panel.id.is_main()) {
1060 heights[primary_index] += removed_height;
1061 self.splits = Self::splits_from_heights(&heights);
1062 } else {
1063 self.ensure_split_count();
1064 }
1065 self.splits = self.normalized_splits(DEFAULT_MIN_PANEL_RATIO);
1066 true
1067 }
1068
1069 pub fn move_panel(&mut self, from_index: usize, to_index: usize) -> bool {
1070 let len = self.panels.len();
1071 if from_index >= len || to_index >= len || from_index == to_index {
1072 return false;
1073 }
1074
1075 self.ensure_split_count();
1076 let mut heights = self.normalized_panel_heights(DEFAULT_MIN_PANEL_RATIO);
1077
1078 let panel = self.panels.remove(from_index);
1079 let panel_height = heights.remove(from_index);
1080
1081 self.panels.insert(to_index, panel);
1082 heights.insert(to_index, panel_height);
1083
1084 self.splits = Self::splits_from_heights(&heights);
1085 self.splits = self.normalized_splits(DEFAULT_MIN_PANEL_RATIO);
1086 true
1087 }
1088
1089 fn new_layer(
1090 &mut self,
1091 name: impl Into<String>,
1092 source: LayerSource,
1093 data_kind: LayerDataKind,
1094 mark: MarkKind,
1095 axis: AxisBinding,
1096 ) -> LayerSpec {
1097 LayerSpec {
1098 id: self.new_layer_id(),
1099 name: name.into(),
1100 source,
1101 data_kind,
1102 purpose: LayerPurpose::Plot,
1103 mark,
1104 mark_override: None,
1105 axis,
1106 visible: true,
1107 style: LayerStyle::default(),
1108 }
1109 }
1110
1111 pub fn sync_comparison_sources(
1112 &mut self,
1113 base_ticker: TickerInfo,
1114 selected_tickers: &[TickerInfo],
1115 ) {
1116 self.rebuild_comparison_bindings(selected_tickers, Some(base_ticker));
1117 }
1118
1119 pub fn copy_comparison_layers_from(&mut self, previous: &Self) {
1120 let Some(primary_panel) = self.primary_panel_id() else {
1121 return;
1122 };
1123 let Some(previous_panel) = previous
1124 .primary_panel_id()
1125 .and_then(|id| previous.panel(id))
1126 else {
1127 return;
1128 };
1129
1130 for layer in &previous_panel.layers {
1131 if matches!(
1132 layer.source,
1133 LayerSource::RawKline {
1134 source: DataSourceId::Comparison(_)
1135 }
1136 ) {
1137 let mut layer = layer.clone();
1138 layer.id = self.new_layer_id();
1139 self.add_layer_to_panel(primary_panel, layer);
1140 }
1141 }
1142 }
1143
1144 pub fn primary_uses_multi_source(&self) -> bool {
1145 self.primary_panel_id()
1146 .and_then(|panel_id| self.panel(panel_id))
1147 .is_some_and(PanelSpec::uses_multi_source)
1148 }
1149
1150 pub fn restore_primary_scale_after_comparisons(&mut self) -> bool {
1151 let Some(panel_id) = self.primary_panel_id() else {
1152 return false;
1153 };
1154 let Some(panel) = self.panel(panel_id) else {
1155 return false;
1156 };
1157 if panel.uses_multi_source() || panel.preferred_scale != PanelScaleMode::PercentFromBase {
1158 return false;
1159 }
1160
1161 self.set_panel_preferred_scale(panel_id, PanelScaleMode::Absolute)
1162 }
1163
1164 pub fn selected_indicators(&self) -> Vec<IndicatorInstanceId> {
1165 let mut indicators = Vec::new();
1166 for panel in &self.panels {
1167 for layer in &panel.layers {
1168 if let Some(value_id) = layer
1169 .source
1170 .indicator_output()
1171 .map(|output| output.instance_id)
1172 && !indicators.contains(&value_id)
1173 {
1174 indicators.push(value_id);
1175 }
1176 }
1177 }
1178 indicators
1179 }
1180
1181 pub fn add_indicator_on_source(
1182 &mut self,
1183 id: IndicatorInstanceId,
1184 indicator: IndicatorId,
1185 source: &crate::chart::kline::IndicatorOutputRef,
1186 ) -> bool {
1187 if self.selected_indicators().contains(&id)
1188 || !matches!(indicator.default_view(), IndicatorPanelRecipe::Plot { .. })
1189 || !indicator.definition().has_numeric_plot_layers()
1190 {
1191 return false;
1192 }
1193
1194 let Some(panel_id) = self.indicator_output_panel_id(source) else {
1195 return false;
1196 };
1197 if self
1198 .panel(panel_id)
1199 .is_none_or(|panel| panel.presentation != PanelPresentation::Plot)
1200 {
1201 return false;
1202 }
1203
1204 let IndicatorPanelRecipe::Plot { layers, .. } = indicator.default_view().clone() else {
1205 return false;
1206 };
1207 match indicator.definition().attachment_mode {
1208 IndicatorAttachmentMode::Overlay => {
1209 self.add_indicator_layers_to_panel(id, layers, panel_id)
1210 }
1211 IndicatorAttachmentMode::Panel => self.add_indicator(id, indicator),
1212 }
1213 }
1214
1215 fn set_panel_mark(&mut self, panel_index: usize, mark: MarkKind) -> bool {
1216 let Some((panel_id, layer_id)) = self.panel_base_layer_ids(panel_index) else {
1217 return false;
1218 };
1219 self.set_panel_layer_mark(panel_id, layer_id, mark)
1220 }
1221
1222 pub fn primary_mark(&self) -> Option<MarkKind> {
1223 let panel_id = self.primary_panel_id()?;
1224 self.panel_effective_mark(panel_id)
1225 }
1226
1227 pub fn set_primary_mark(&mut self, mark: MarkKind) -> bool {
1228 let Some(primary_panel_index) = self.panels.iter().position(|panel| panel.id.is_main())
1229 else {
1230 return false;
1231 };
1232
1233 self.set_panel_mark(primary_panel_index, mark)
1234 }
1235
1236 pub fn reset_primary_mark(&mut self) -> bool {
1237 let Some(panel_id) = self.primary_panel_id() else {
1238 return false;
1239 };
1240 let Some(layer_id) = self.panel(panel_id).and_then(|panel| {
1241 panel
1242 .base_layer
1243 .or_else(|| panel.layers.first().map(|layer| layer.id))
1244 }) else {
1245 return false;
1246 };
1247
1248 self.clear_panel_layer_mark(panel_id, layer_id)
1249 }
1250
1251 fn indicator_locations(
1252 &self,
1253 id: IndicatorInstanceId,
1254 ) -> impl Iterator<Item = (PanelId, LayerId)> + '_ {
1255 self.panels.iter().flat_map(move |panel| {
1256 panel
1257 .layers
1258 .iter()
1259 .filter(move |layer| layer.source.references_instance(id))
1260 .map(move |layer| (panel.id, layer.id))
1261 })
1262 }
1263
1264 fn indicator_layer_binding(&self, id: IndicatorInstanceId) -> Option<(PanelId, LayerId)> {
1265 self.indicator_locations(id).next()
1266 }
1267
1268 fn indicator_mark_layer_ids(
1269 &self,
1270 id: IndicatorInstanceId,
1271 kind: Option<&IndicatorId>,
1272 mark: Option<MarkKind>,
1273 ) -> Option<(PanelId, Vec<LayerId>)> {
1274 let (panel_id, _) = self.indicator_layer_binding(id)?;
1275 let panel = self.panel(panel_id)?;
1276 let output_layers = panel
1277 .layers
1278 .iter()
1279 .filter(|layer| layer.source.references_instance(id))
1280 .collect::<Vec<_>>();
1281 if output_layers.is_empty() {
1282 return None;
1283 }
1284
1285 if kind.is_none() {
1286 return Some((
1287 panel_id,
1288 output_layers.iter().map(|layer| layer.id).collect(),
1289 ));
1290 }
1291
1292 let source_channel = mark
1293 .and_then(|mark| kind.and_then(|kind| kind.mark_source_channel(mark)))
1294 .or_else(|| {
1295 kind.and_then(|kind| {
1296 output_layers.iter().find_map(|layer| {
1297 let channel = layer.source.indicator_output()?.channel_id.as_str();
1298 kind.mark_specs()
1299 .iter()
1300 .any(|spec| spec.source_channel == channel)
1301 .then_some(channel)
1302 })
1303 })
1304 });
1305 let matching_layers = source_channel.and_then(|source_channel| {
1306 let matching = output_layers
1307 .iter()
1308 .filter(|layer| {
1309 layer
1310 .source
1311 .indicator_output()
1312 .is_some_and(|output| output.channel_id == source_channel)
1313 })
1314 .map(|layer| layer.id)
1315 .collect::<Vec<_>>();
1316 (!matching.is_empty()).then_some(matching)
1317 });
1318 let layer_ids = matching_layers.unwrap_or_else(|| {
1319 if output_layers.len() == 1 {
1320 output_layers.iter().map(|layer| layer.id).collect()
1321 } else {
1322 output_layers
1323 .first()
1324 .map(|layer| vec![layer.id])
1325 .unwrap_or_default()
1326 }
1327 });
1328 Some((panel_id, layer_ids))
1329 }
1330
1331 pub fn indicator_mark(
1332 &self,
1333 id: IndicatorInstanceId,
1334 kind: Option<&IndicatorId>,
1335 ) -> Option<MarkKind> {
1336 let (panel_id, layer_ids) = self.indicator_mark_layer_ids(id, kind, None)?;
1337
1338 self.resolved_panel_marks(panel_id)?
1339 .into_iter()
1340 .find(|(candidate_layer_id, _)| layer_ids.contains(candidate_layer_id))
1341 .map(|(_, mark)| mark)
1342 }
1343
1344 pub fn indicator_mark_target_layer_ids(
1345 &self,
1346 id: IndicatorInstanceId,
1347 kind: &IndicatorId,
1348 mark: MarkKind,
1349 ) -> Option<(PanelId, Vec<LayerId>)> {
1350 self.indicator_mark_layer_ids(id, Some(kind), Some(mark))
1351 }
1352
1353 pub fn set_indicator_mark(
1354 &mut self,
1355 id: IndicatorInstanceId,
1356 mark: MarkKind,
1357 kind: Option<&IndicatorId>,
1358 ) -> bool {
1359 let Some((panel_id, layer_ids)) = self.indicator_mark_layer_ids(id, kind, Some(mark))
1360 else {
1361 return false;
1362 };
1363
1364 let Some(panel) = self.panel_mut(panel_id) else {
1365 return false;
1366 };
1367 let mut changed = false;
1368 for layer in &mut panel.layers {
1369 if layer_ids.contains(&layer.id) && layer.mark_override != Some(mark) {
1370 layer.mark_override = Some(mark);
1371 changed = true;
1372 }
1373 }
1374 changed
1375 }
1376
1377 pub fn reset_indicator_mark(&mut self, id: IndicatorInstanceId) -> bool {
1378 let Some((panel_id, layer_id)) = self.indicator_layer_binding(id) else {
1379 return false;
1380 };
1381
1382 let Some(panel) = self.panel_mut(panel_id) else {
1383 return false;
1384 };
1385 let has_output_layers = panel
1386 .layers
1387 .iter()
1388 .any(|layer| layer.source.references_instance(id));
1389 if !has_output_layers {
1390 return panel
1391 .layers
1392 .iter_mut()
1393 .find(|layer| layer.id == layer_id)
1394 .map(|layer| layer.mark_override.take().is_some())
1395 .unwrap_or(false);
1396 }
1397
1398 let mut changed = false;
1399 for layer in &mut panel.layers {
1400 if layer.source.references_instance(id) {
1401 changed |= layer.mark_override.take().is_some();
1402 }
1403 }
1404 changed
1405 }
1406
1407 pub fn toggle_indicator_visibility(&mut self, id: IndicatorInstanceId) -> bool {
1408 let Some((panel_id, _)) = self.indicator_layer_binding(id) else {
1409 return false;
1410 };
1411 let Some(panel) = self.panel_mut(panel_id) else {
1412 return false;
1413 };
1414 let visible = !panel.indicator_visible(id);
1415 let mut changed = false;
1416 for layer in &mut panel.layers {
1417 if layer.source.references_instance(id) && layer.visible != visible {
1418 layer.visible = visible;
1419 changed = true;
1420 }
1421 }
1422 changed
1423 }
1424
1425 pub fn add_indicator(&mut self, id: IndicatorInstanceId, indicator: IndicatorId) -> bool {
1426 if self.selected_indicators().contains(&id) {
1427 return false;
1428 }
1429
1430 let definition = indicator.definition();
1431 match definition.panel_recipe.clone() {
1432 IndicatorPanelRecipe::Plot {
1433 panel_title,
1434 value_precision,
1435 value_label_policy,
1436 preferred_scale,
1437 layers: declared_layers,
1438 ..
1439 } => {
1440 if declared_layers.is_empty() {
1441 return false;
1442 }
1443 let pane = declared_layers
1444 .first()
1445 .map(|layer| layer.pane)
1446 .unwrap_or(PaneTarget::Indicator);
1447 match pane {
1448 PaneTarget::Main => {
1449 let Some(panel_id) = self.primary_panel_id() else {
1450 return false;
1451 };
1452 if !self.add_indicator_layers_to_panel(id, declared_layers, panel_id) {
1453 return false;
1454 }
1455 }
1456 PaneTarget::Indicator => {
1457 let layers = self.new_indicator_layers(id, declared_layers);
1458 let panel_id =
1459 self.next_indicator_panel_id(&indicator, PanelPresentation::Plot);
1460 if self
1461 .add_aux_panel(
1462 panel_id,
1463 panel_title,
1464 layers,
1465 Some(value_precision),
1466 value_label_policy,
1467 preferred_scale,
1468 PanelPresentation::Plot,
1469 )
1470 .is_none()
1471 {
1472 return false;
1473 }
1474 }
1475 PaneTarget::Named(named_pane) => {
1476 let panel_id = self.named_panel_id(named_pane).or_else(|| {
1477 self.add_named_panel(
1478 named_pane,
1479 panel_title,
1480 value_precision,
1481 value_label_policy,
1482 preferred_scale,
1483 )
1484 });
1485 let Some(panel_id) = panel_id else {
1486 return false;
1487 };
1488 if !self.add_indicator_layers_to_panel(id, declared_layers, panel_id) {
1489 return false;
1490 }
1491 }
1492 }
1493 }
1494 IndicatorPanelRecipe::Table {
1495 panel_title,
1496 columns,
1497 } => {
1498 let Some(channel_id) = columns.first().map(|column| column.value.clone()) else {
1499 return false;
1500 };
1501 let Some(panel_id) =
1502 self.add_table_panel(panel_title, DataSourceId::Primary, &indicator)
1503 else {
1504 return false;
1505 };
1506 if let Some(panel) = self.panel_mut(panel_id)
1507 && let Some(layer) = panel.layers.first_mut()
1508 {
1509 layer.source = LayerSource::IndicatorOutput {
1510 source: DataSourceId::Primary,
1511 output: crate::chart::kline::IndicatorOutputRef::new(id, channel_id),
1512 };
1513 }
1514 }
1515 IndicatorPanelRecipe::PrimaryOverlay {
1516 layers: declared_layers,
1517 ..
1518 } => {
1519 let Some(primary_panel_id) = self.primary_panel_id() else {
1520 return false;
1521 };
1522 if declared_layers.is_empty() {
1523 return false;
1524 }
1525 for spec in declared_layers.iter().cloned() {
1526 let layer = self.new_layer(
1527 spec.layer_name,
1528 LayerSource::IndicatorOutput {
1529 source: DataSourceId::Primary,
1530 output: crate::chart::kline::IndicatorOutputRef::new(
1531 id,
1532 spec.channel_id,
1533 ),
1534 },
1535 spec.data_kind,
1536 spec.mark,
1537 spec.axis,
1538 );
1539 if !self.add_layer_to_panel(primary_panel_id, layer) {
1540 return false;
1541 }
1542 }
1543 }
1544 }
1545 true
1546 }
1547
1548 fn new_indicator_layers(
1549 &mut self,
1550 id: IndicatorInstanceId,
1551 declared_layers: Vec<crate::chart::kline::IndicatorOutputLayerSpec>,
1552 ) -> Vec<crate::chart::kline::composition::LayerSpec> {
1553 declared_layers
1554 .into_iter()
1555 .map(|spec| {
1556 self.new_layer(
1557 spec.layer_name,
1558 LayerSource::IndicatorOutput {
1559 source: DataSourceId::Primary,
1560 output: crate::chart::kline::IndicatorOutputRef::new(id, spec.channel_id),
1561 },
1562 spec.data_kind,
1563 spec.mark,
1564 spec.axis,
1565 )
1566 })
1567 .collect()
1568 }
1569
1570 fn add_indicator_layers_to_panel(
1571 &mut self,
1572 id: IndicatorInstanceId,
1573 declared_layers: Vec<crate::chart::kline::IndicatorOutputLayerSpec>,
1574 panel_id: PanelId,
1575 ) -> bool {
1576 self.new_indicator_layers(id, declared_layers)
1577 .into_iter()
1578 .all(|layer| self.add_layer_to_panel(panel_id, layer))
1579 }
1580
1581 pub fn indicator_panel_id(&self, id: IndicatorInstanceId) -> Option<PanelId> {
1582 self.indicator_locations(id)
1583 .next()
1584 .map(|(panel_id, _)| panel_id)
1585 }
1586
1587 pub fn indicator_output_panel_id(&self, output: &IndicatorOutputRef) -> Option<PanelId> {
1588 self.panels
1589 .iter()
1590 .find(|panel| {
1591 panel
1592 .layers
1593 .iter()
1594 .any(|layer| layer.source.indicator_output() == Some(output))
1595 })
1596 .map(|panel| panel.id)
1597 }
1598
1599 pub fn reconcile_attached_indicator_placements(
1600 &mut self,
1601 instances: &IndicatorInstanceStore,
1602 ) -> bool {
1603 let attached_ids = instances
1604 .instances()
1605 .iter()
1606 .filter(|instance| !instance.source_bindings.is_empty())
1607 .map(|instance| instance.id)
1608 .collect::<Vec<_>>();
1609 let mut changed = false;
1610
1611 for id in attached_ids {
1612 let Some(instance) = instances.get(id) else {
1613 continue;
1614 };
1615 let overlay = matches!(
1616 instance.kind.definition().attachment_mode,
1617 IndicatorAttachmentMode::Overlay
1618 );
1619 let mut source_panel = None;
1620 let mut valid = instance.kind.definition().has_numeric_plot_layers()
1621 && matches!(
1622 instance.kind.default_view(),
1623 IndicatorPanelRecipe::Plot { .. }
1624 );
1625 for (input_id, source) in &instance.source_bindings {
1626 if !instance
1627 .kind
1628 .definition()
1629 .source_inputs
1630 .iter()
1631 .any(|input| input.id == *input_id && input.allow_indicator)
1632 || source.instance_id == id
1633 || instances
1634 .get(source.instance_id)
1635 .is_none_or(|source_instance| {
1636 source_instance
1637 .kind
1638 .definition()
1639 .sourceable_numeric_output(&source.channel_id)
1640 .is_none()
1641 })
1642 {
1643 valid = false;
1644 break;
1645 }
1646
1647 let Some(panel_id) = self.indicator_output_panel_id(source) else {
1648 valid = false;
1649 break;
1650 };
1651 if self
1652 .panel(panel_id)
1653 .is_none_or(|panel| panel.presentation != PanelPresentation::Plot)
1654 || (overlay && source_panel.is_some_and(|target| target != panel_id))
1655 {
1656 valid = false;
1657 break;
1658 }
1659 source_panel = Some(panel_id);
1660 }
1661
1662 if !valid {
1663 continue;
1664 }
1665 let Some(target_panel) = (if overlay {
1666 source_panel
1667 } else {
1668 self.standalone_indicator_panel(id, &instance.kind, instances)
1669 }) else {
1670 continue;
1671 };
1672 changed |= self.ensure_indicator_layers_to_panel(id, &instance.kind, target_panel);
1673 }
1674
1675 changed
1676 }
1677
1678 fn standalone_indicator_panel(
1679 &mut self,
1680 id: IndicatorInstanceId,
1681 indicator: &IndicatorId,
1682 instances: &IndicatorInstanceStore,
1683 ) -> Option<PanelId> {
1684 let IndicatorPanelRecipe::Plot {
1685 panel_title,
1686 value_precision,
1687 value_label_policy,
1688 preferred_scale,
1689 layers,
1690 ..
1691 } = indicator.default_view().clone()
1692 else {
1693 return None;
1694 };
1695 let pane = layers.first()?.pane;
1696
1697 match pane {
1698 PaneTarget::Main => self.primary_panel_id(),
1699 PaneTarget::Named(named_pane) => self.named_panel_id(named_pane).or_else(|| {
1700 self.add_named_panel(
1701 named_pane,
1702 panel_title,
1703 value_precision,
1704 value_label_policy,
1705 preferred_scale,
1706 )
1707 }),
1708 PaneTarget::Indicator => {
1709 let source_instances = instances
1710 .get(id)
1711 .into_iter()
1712 .flat_map(|instance| instance.source_bindings.values())
1713 .map(|source| source.instance_id)
1714 .collect::<FxHashSet<_>>();
1715 self.panels
1716 .iter()
1717 .find(|panel| {
1718 panel.id.is_aux()
1719 && panel.presentation == PanelPresentation::Plot
1720 && panel
1721 .layers
1722 .iter()
1723 .any(|layer| layer.source.references_instance(id))
1724 && panel
1725 .layers
1726 .iter()
1727 .filter_map(|layer| layer.source.indicator_output())
1728 .all(|output| {
1729 output.instance_id == id
1730 || !source_instances.contains(&output.instance_id)
1731 })
1732 })
1733 .map(|panel| panel.id)
1734 .or_else(|| {
1735 let panel_id =
1736 self.next_indicator_panel_id(indicator, PanelPresentation::Plot);
1737 self.add_aux_panel(
1738 panel_id,
1739 panel_title,
1740 Vec::new(),
1741 Some(value_precision),
1742 value_label_policy,
1743 preferred_scale,
1744 PanelPresentation::Plot,
1745 )
1746 })
1747 }
1748 }
1749 }
1750
1751 fn ensure_indicator_layers_to_panel(
1752 &mut self,
1753 id: IndicatorInstanceId,
1754 indicator: &IndicatorId,
1755 target_panel: PanelId,
1756 ) -> bool {
1757 let IndicatorPanelRecipe::Plot { layers, .. } = indicator.default_view().clone() else {
1758 return false;
1759 };
1760 let mut existing_channels = self
1761 .panels
1762 .iter()
1763 .flat_map(|panel| panel.layers.iter())
1764 .filter(|layer| layer.source.references_instance(id))
1765 .filter_map(|layer| layer.source.indicator_output())
1766 .map(|output| output.channel_id.clone())
1767 .collect::<FxHashSet<_>>();
1768 let mut changed = false;
1769
1770 for spec in layers {
1771 if !existing_channels.insert(spec.channel_id.clone()) {
1772 continue;
1773 }
1774 let layer = self.new_layer(
1775 spec.layer_name,
1776 LayerSource::IndicatorOutput {
1777 source: DataSourceId::Primary,
1778 output: crate::chart::kline::IndicatorOutputRef::new(id, spec.channel_id),
1779 },
1780 spec.data_kind,
1781 spec.mark,
1782 spec.axis,
1783 );
1784 changed |= self.add_layer_to_panel(target_panel, layer);
1785 }
1786
1787 changed | self.move_indicator_layers_to_panel(id, target_panel)
1788 }
1789
1790 fn move_indicator_layers_to_panel(
1791 &mut self,
1792 id: IndicatorInstanceId,
1793 target_panel: PanelId,
1794 ) -> bool {
1795 let locations = self.indicator_locations(id).collect::<Vec<_>>();
1796 if locations.is_empty()
1797 || locations
1798 .iter()
1799 .all(|(panel_id, _)| *panel_id == target_panel)
1800 {
1801 return false;
1802 }
1803
1804 let layers = locations
1805 .iter()
1806 .filter_map(|(panel_id, layer_id)| {
1807 self.panel(*panel_id)
1808 .and_then(|panel| panel.layers.iter().find(|layer| layer.id == *layer_id))
1809 .cloned()
1810 })
1811 .collect::<Vec<_>>();
1812 for (panel_id, layer_id) in &locations {
1813 let _ = self.remove_layer_from_panel(*panel_id, *layer_id);
1814 }
1815 for layer in layers {
1816 let _ = self.add_layer_to_panel(target_panel, layer);
1817 }
1818
1819 let touched_panels = locations
1820 .iter()
1821 .map(|(panel_id, _)| *panel_id)
1822 .collect::<FxHashSet<_>>();
1823 for panel_id in touched_panels {
1824 if self
1825 .panel(panel_id)
1826 .is_some_and(|panel| panel.id.is_aux() && panel.layers.is_empty())
1827 {
1828 let _ = self.remove_panel(panel_id);
1829 }
1830 }
1831 true
1832 }
1833
1834 pub fn remove_indicator(&mut self, id: IndicatorInstanceId) -> bool {
1835 let locations = self.indicator_locations(id).collect::<Vec<_>>();
1836 if locations.is_empty() {
1837 return false;
1838 }
1839 let touched_panels = locations
1840 .iter()
1841 .map(|(panel_id, _)| *panel_id)
1842 .collect::<FxHashSet<_>>();
1843 let mut removed = false;
1844 for (panel_id, layer_id) in locations {
1845 removed |= self.remove_layer_from_panel(panel_id, layer_id);
1846 }
1847 let empty_panels = self
1848 .panels
1849 .iter()
1850 .filter(|panel| {
1851 touched_panels.contains(&panel.id) && panel.id.is_aux() && panel.layers.is_empty()
1852 })
1853 .map(|panel| panel.id)
1854 .collect::<Vec<_>>();
1855 for panel_id in empty_panels {
1856 removed |= self.remove_panel(panel_id);
1857 }
1858 removed
1859 }
1860
1861 fn rebuild_comparison_bindings(
1862 &mut self,
1863 selected_tickers: &[TickerInfo],
1864 base_ticker: Option<TickerInfo>,
1865 ) {
1866 let Some(primary_panel_id) = self.primary_panel_id() else {
1867 return;
1868 };
1869 let Some(primary_panel) = self.panel(primary_panel_id) else {
1870 return;
1871 };
1872
1873 let stale_layer_ids: Vec<LayerId> = primary_panel
1874 .layers
1875 .iter()
1876 .filter_map(|layer| {
1877 let LayerSource::RawKline {
1878 source: DataSourceId::Comparison(source),
1879 } = layer.source
1880 else {
1881 return None;
1882 };
1883 let is_selected_comparison = base_ticker.is_some_and(|base| {
1884 selected_tickers
1885 .iter()
1886 .any(|ticker| *ticker != base && ticker.ticker == source)
1887 });
1888 (!is_selected_comparison).then_some(layer.id)
1889 })
1890 .collect();
1891
1892 for layer_id in stale_layer_ids {
1893 let _ = self.remove_layer_from_panel(primary_panel_id, layer_id);
1894 }
1895
1896 let Some(base_ticker) = base_ticker else {
1897 return;
1898 };
1899
1900 for &ticker in selected_tickers
1901 .iter()
1902 .filter(|ticker| **ticker != base_ticker)
1903 {
1904 if self.comparison_layer(ticker).is_some() {
1905 continue;
1906 }
1907 let name = ticker.ticker.symbol_and_exchange_string();
1908 let _ = self.add_comparison_source_to_panel(
1909 primary_panel_id,
1910 DataSourceId::Comparison(ticker.ticker),
1911 name,
1912 );
1913 }
1914 }
1915
1916 fn panel_base_layer_ids(&self, panel_index: usize) -> Option<(PanelId, LayerId)> {
1917 let panel = self.panels.get(panel_index)?;
1918 let layer_id = panel
1919 .base_layer
1920 .or_else(|| panel.layers.first().map(|layer| layer.id))?;
1921 Some((panel.id, layer_id))
1922 }
1923
1924 fn ensure_split_count(&mut self) {
1925 let target = self.split_count();
1926 match self.splits.len().cmp(&target) {
1927 Ordering::Equal => {}
1928 Ordering::Greater => self.splits.truncate(target),
1929 Ordering::Less => {
1930 for index in self.splits.len()..target {
1931 let fallback = (index + 1) as f32 / (target + 1) as f32;
1932 self.splits.push(fallback);
1933 }
1934 }
1935 }
1936 }
1937
1938 fn normalized_panel_heights(&self, min_panel_ratio: f32) -> Vec<f32> {
1939 let panel_count = self.panel_count();
1940 if panel_count == 0 {
1941 return Vec::new();
1942 }
1943
1944 let mut heights = Vec::with_capacity(panel_count);
1945 let mut previous = 0.0;
1946
1947 for split in self.normalized_splits(min_panel_ratio) {
1948 let clamped = split.clamp(previous, 1.0);
1949 heights.push((clamped - previous).max(0.0));
1950 previous = clamped;
1951 }
1952
1953 heights.push((1.0 - previous).max(0.0));
1954
1955 let total: f32 = heights.iter().sum();
1956 if total > f32::EPSILON {
1957 for height in &mut heights {
1958 *height /= total;
1959 }
1960 }
1961
1962 heights
1963 }
1964
1965 fn splits_from_heights(heights: &[f32]) -> Vec<f32> {
1966 if heights.len() <= 1 {
1967 return Vec::new();
1968 }
1969
1970 let total: f32 = heights.iter().copied().sum();
1971 if total <= f32::EPSILON {
1972 let count = heights.len();
1973 return (1..count)
1974 .map(|index| index as f32 / count as f32)
1975 .collect();
1976 }
1977
1978 let mut splits = Vec::with_capacity(heights.len().saturating_sub(1));
1979 let mut acc = 0.0;
1980 for height in heights.iter().take(heights.len().saturating_sub(1)) {
1981 acc += *height / total;
1982 splits.push(acc.clamp(0.0, 1.0));
1983 }
1984
1985 splits
1986 }
1987
1988 fn new_layer_id(&mut self) -> LayerId {
1989 let id = LayerId(self.next_layer_id);
1990 self.next_layer_id = self.next_layer_id.wrapping_add(1);
1991 id
1992 }
1993}