Skip to main content

flowsurface_data/chart/kline/
composition.rs

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    /// Internal output binding used to identify and populate a table panel.
69    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    /// Marks that consume a bar-anchored numeric series. Sparse outputs are
104    /// joined only across consecutive available samples by the renderer.
105    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/// Chart panels and their cumulative height ratios.
603///
604/// Adding an auxiliary panel takes height from the primary; removing one
605/// returns its height to the primary. Surviving auxiliary panels retain
606/// their ratios, subject to minimum-height normalization. Reordering
607/// carries each panel's ratio with it. Heights are proportional, not pixels.
608///
609/// New auxiliary panels receive up to 15%, reduced to an 8% minimum when
610/// space is limited. The primary must retain at least 8%.
611#[derive(Debug, Clone)]
612pub struct ChartComposition {
613    pub panels: Vec<PanelSpec>,
614    /// Normalized boundaries in ascending order, one fewer than panel count.
615    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    /// Returns `None` if no primary panel exists or it cannot supply the
849    /// minimum auxiliary height while retaining its own minimum height.
850    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}