1mod controls;
4mod indicator;
5mod render;
6pub use indicator::{IndicatorCoordParams, IndicatorPane, IndicatorPaneConfig, PaneInteraction};
7pub use render::RenderedIndicatorPane;
8
9use super::volume;
10use super::{Chart, SeriesId};
11#[cfg(feature = "drawings")]
12use crate::drawings::persistence::StoredPane;
13use egui::{Color32, Rect, Vec2};
14
15type PaneStudy = (String, Vec<(String, serde_json::Value)>);
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
22pub struct PaneId(pub(super) usize);
23
24impl PaneId {
25 pub const MAIN: PaneId = PaneId(0);
27}
28
29#[derive(Clone, Copy, Debug, PartialEq)]
32pub struct PaneOptions {
33 pub resizable: bool,
36 pub separator_color: Option<Color32>,
38 pub separator_hover_color: Option<Color32>,
41}
42
43impl Default for PaneOptions {
44 fn default() -> Self {
45 Self {
46 resizable: true,
47 separator_color: None,
48 separator_hover_color: None,
49 }
50 }
51}
52
53#[derive(Clone)]
54pub(super) struct PaneLayout {
55 pub(super) panes: Vec<Pane>,
57 pub(super) maximized: Option<PaneId>,
58 pub(super) main: Vec<SeriesId>,
60}
61
62impl Default for PaneLayout {
63 fn default() -> Self {
64 Self {
65 panes: vec![Pane::new(PaneId::MAIN)],
66 maximized: None,
67 main: vec![SeriesId::MAIN],
68 }
69 }
70}
71
72#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75pub enum VisualOrder {
76 BringToFront,
78 BringForward,
80 SendBackward,
82 SendToBack,
84}
85
86#[derive(Clone)]
87pub(super) struct Pane {
88 pub(super) id: PaneId,
89 pub(super) members: Vec<SeriesId>,
91 pub(super) stretch: f32,
93 pub(super) height: Option<f32>,
95 pub(super) preserve: bool,
97 pub(super) collapsed: Option<bool>,
98 pub(super) scale: crate::scales::PriceScale,
99 pub(super) rect: Rect,
101}
102
103impl Pane {
104 fn new(id: PaneId) -> Self {
105 Self {
106 id,
107 members: Vec::new(),
108 stretch: Self::default_stretch(id),
109 height: None,
110 preserve: false,
111 collapsed: None,
112 scale: crate::scales::PriceScale::new(crate::scales::PriceScaleOptions {
113 scale_margins: crate::scales::PriceScaleMargins {
114 top: 0.0,
115 bottom: 0.0,
116 },
117 ..Default::default()
118 }),
119 rect: Rect::NOTHING,
120 }
121 }
122
123 pub(super) fn price_map(&self) -> crate::scales::PriceMap {
125 self.scale
126 .map(self.scale.transform(), self.rect.top(), self.rect.bottom())
127 }
128
129 pub(super) fn default_stretch(id: PaneId) -> f32 {
131 if id == PaneId::MAIN { 3.0 } else { 1.0 }
132 }
133}
134
135impl Chart {
136 fn pane_member(&self, id: SeriesId) -> bool {
139 self.price_series.iter().any(|item| item.id == id)
140 || self
141 .study_ids
142 .iter()
143 .position(|study| *study == id)
144 .is_some_and(|idx| !self.studies.indicators()[idx].is_overlay())
145 }
146
147 pub(super) fn in_pane(&self, id: SeriesId) -> bool {
149 self.pane_layout
150 .panes
151 .iter()
152 .any(|pane| pane.members.contains(&id))
153 }
154
155 fn pane_listed(&self, pane: &Pane) -> bool {
158 pane.preserve
159 || pane.members.is_empty()
160 || pane.members.iter().any(|id| self.pane_member(*id))
161 }
162
163 pub(super) fn pane_shown(&self, pane: &Pane) -> bool {
165 pane.id != PaneId::MAIN
166 && self.pane_listed(pane)
167 && (pane.members.is_empty()
168 || pane
169 .members
170 .iter()
171 .any(|id| self.pane_member(*id) && self.is_series_visible(*id)))
172 }
173
174 pub(super) fn has_panes(&self) -> bool {
176 self.pane_layout
177 .panes
178 .iter()
179 .any(|pane| self.pane_shown(pane))
180 }
181
182 fn pane_index(&self, pane: PaneId) -> Option<usize> {
183 self.pane_layout
184 .panes
185 .iter()
186 .position(|item| item.id == pane && self.pane_listed(item))
187 }
188
189 fn member_name(&self, id: SeriesId) -> &str {
191 if let Some(idx) = self.study_ids.iter().position(|study| *study == id) {
192 self.studies.indicators()[idx].name()
193 } else {
194 self.price_series
195 .iter()
196 .find(|item| item.id == id)
197 .map_or("Series", |item| item.options.name.as_str())
198 }
199 }
200
201 fn leave_panes(&mut self, id: SeriesId) {
203 self.pane_layout.panes.retain_mut(|pane| {
204 let before = pane.members.len();
205 pane.members.retain(|member| *member != id);
206 pane.members.len() == before || !pane.members.is_empty() || pane.preserve
207 });
208 }
209
210 pub(super) fn sync_panes(&mut self) {
211 let mut emptied = Vec::new();
212 for at in 0..self.pane_layout.panes.len() {
213 let mut members = std::mem::take(&mut self.pane_layout.panes[at].members);
214 let before = members.len();
215 members.retain(|id| self.pane_member(*id));
216 let pane = &mut self.pane_layout.panes[at];
217 if before > 0 && members.is_empty() && !pane.preserve {
218 emptied.push(pane.id);
219 }
220 pane.members = members;
221 }
222 self.pane_layout
223 .panes
224 .retain(|pane| !emptied.contains(&pane.id));
225 for idx in 0..self.study_ids.len() {
226 let id = self.study_ids[idx];
227 if !self.studies.indicators()[idx].is_overlay() && !self.in_pane(id) {
228 self.add_study_pane(id);
229 }
230 }
231 let overlays: Vec<_> = self
232 .study_ids
233 .iter()
234 .zip(self.studies.indicators())
235 .filter(|(_, study)| study.is_overlay())
236 .map(|(id, _)| *id)
237 .collect();
238 let series: Vec<_> = self.main_pane_series().map(|item| item.id).collect();
239 let main = &mut self.pane_layout.main;
240 main.retain(|id| *id == SeriesId::MAIN || overlays.contains(id) || series.contains(id));
241 if !main.contains(&SeriesId::MAIN) {
242 main.insert(0, SeriesId::MAIN);
243 }
244 let mut after = main
245 .iter()
246 .position(|id| *id == SeriesId::MAIN)
247 .unwrap_or(0)
248 + 1;
249 for id in series {
250 if !main.contains(&id) {
251 main.insert(after.min(main.len()), id);
252 }
253 after = main.iter().position(|member| *member == id).unwrap_or(0) + 1;
254 }
255 for id in overlays {
256 if !main.contains(&id) {
257 main.push(id);
258 }
259 }
260 if self.pane_layout.maximized.is_some_and(|id| {
261 id != PaneId::MAIN
262 && !self
263 .pane_layout
264 .panes
265 .iter()
266 .any(|pane| pane.id == id && self.pane_shown(pane))
267 }) {
268 self.pane_layout.maximized = None;
269 }
270 }
271
272 fn study_pane_height(&self, id: SeriesId) -> Option<f32> {
274 self.study_ids
275 .iter()
276 .position(|study| *study == id)
277 .and_then(|idx| self.studies.indicators()[idx].pane().height)
278 .filter(|height| height.is_finite() && *height >= 40.0)
279 }
280
281 pub(super) fn add_study_pane(&mut self, id: SeriesId) {
283 let height = self.study_pane_height(id);
284 self.add_pane();
285 let pane = self.pane_layout.panes.last_mut().expect("pane was added");
286 pane.height = height;
287 pane.members.push(id);
288 }
289
290 pub fn add_pane(&mut self) -> PaneId {
295 let id = PaneId(self.next_series);
296 self.next_series += 1;
297 self.pane_layout.panes.push(Pane::new(id));
298 id
299 }
300
301 pub fn remove_pane(&mut self, pane: PaneId) -> bool {
304 self.sync_panes();
305 if pane == PaneId::MAIN {
306 return false;
307 }
308 let Some(at) = self.pane_index(pane) else {
309 return false;
310 };
311 for id in self.pane_layout.panes[at].members.clone() {
312 self.remove_series(id);
313 }
314 self.pane_layout.panes.retain(|item| item.id != pane);
315 self.sync_panes();
316 true
317 }
318
319 pub fn panes(&self) -> Vec<PaneId> {
322 self.pane_layout
323 .panes
324 .iter()
325 .filter(|pane| self.pane_listed(pane))
326 .map(|pane| pane.id)
327 .collect()
328 }
329
330 pub fn pane_series(&self, pane: PaneId) -> Vec<SeriesId> {
334 if pane == PaneId::MAIN {
335 self.main_series_present
336 .then_some(SeriesId::MAIN)
337 .into_iter()
338 .chain(
339 self.price_series
340 .iter()
341 .map(|item| item.id)
342 .filter(|id| !self.in_pane(*id)),
343 )
344 .chain(
345 self.indicators()
346 .filter(|(_, study)| study.is_overlay())
347 .map(|(id, _)| id),
348 )
349 .collect()
350 } else {
351 self.pane_index(pane)
352 .map(|at| {
353 self.pane_layout.panes[at]
354 .members
355 .iter()
356 .copied()
357 .filter(|id| self.pane_member(*id))
358 .collect()
359 })
360 .unwrap_or_default()
361 }
362 }
363
364 pub fn linked_pane(&self, pane: PaneId, other: &Chart) -> Option<PaneId> {
371 let (studies, rank) = self.pane_identity(pane)?;
372 other.identified_pane(&studies, rank)
373 }
374
375 fn pane_identity(&self, pane: PaneId) -> Option<(Vec<PaneStudy>, usize)> {
378 if pane == PaneId::MAIN {
379 return None;
380 }
381 let studies = self.pane_studies(pane);
382 if studies.is_empty() {
383 return None;
384 }
385 let rank = self
386 .panes()
387 .into_iter()
388 .take_while(|other| *other != pane)
389 .filter(|other| *other != PaneId::MAIN && self.pane_studies(*other) == studies)
390 .count();
391 Some((studies, rank))
392 }
393
394 fn identified_pane(&self, studies: &[PaneStudy], rank: usize) -> Option<PaneId> {
396 self.panes()
397 .into_iter()
398 .filter(|pane| *pane != PaneId::MAIN && self.pane_studies(*pane) == studies)
399 .nth(rank)
400 }
401
402 #[cfg(feature = "drawings")]
404 pub(crate) fn stored_pane(&self, pane: PaneId) -> Option<StoredPane> {
405 self.pane_identity(pane)
406 .map(|(studies, rank)| StoredPane { studies, rank })
407 }
408
409 #[cfg(feature = "drawings")]
412 pub(crate) fn stored_pane_owner(&self, stored: &StoredPane) -> Option<usize> {
413 let pane = self.identified_pane(&stored.studies, stored.rank)?;
414 self.pane_series(pane).first().map(|id| id.0)
415 }
416
417 fn pane_studies(&self, pane: PaneId) -> Vec<PaneStudy> {
419 self.pane_series(pane)
420 .into_iter()
421 .filter_map(|id| self.indicators().find(|(study, _)| *study == id))
422 .map(|(_, study)| {
423 let inputs = study.inputs().into_iter().map(|input| {
425 let text = serde_json::to_string(&input.value).unwrap_or_default();
426 (input.id, serde_json::from_str(&text).unwrap_or_default())
427 });
428 (study.name().to_owned(), inputs.collect())
429 })
430 .collect()
431 }
432
433 pub fn series_pane(&self, series: SeriesId) -> Option<PaneId> {
435 if let Some(pane) = self
436 .pane_layout
437 .panes
438 .iter()
439 .find(|pane| pane.members.contains(&series))
440 .filter(|_| self.pane_member(series))
441 {
442 return Some(pane.id);
443 }
444 let main = if series == SeriesId::MAIN {
445 self.main_series_present
446 } else {
447 self.price_series.iter().any(|item| item.id == series)
448 || self
449 .indicators()
450 .any(|(id, study)| id == series && study.is_overlay())
451 };
452 main.then_some(PaneId::MAIN)
453 }
454
455 pub fn move_series_to_pane(&mut self, series: SeriesId, pane: PaneId) -> bool {
461 self.sync_panes();
462 if self.pane_index(pane).is_none() {
463 return false;
464 }
465 if let Some(idx) = self.study_ids.iter().position(|id| *id == series) {
466 self.record_study(series);
467 if !self.studies.indicators_mut()[idx].set_overlay(pane == PaneId::MAIN) {
468 return false;
469 }
470 } else if !self.price_series.iter().any(|item| item.id == series) {
471 return false;
472 }
473 if self.series_pane(series) != Some(pane) {
474 self.leave_panes(series);
475 if let Some(target) = self
476 .pane_layout
477 .panes
478 .iter_mut()
479 .find(|item| item.id == pane && pane != PaneId::MAIN)
480 {
481 target.members.push(series);
482 }
483 }
484 self.sync_panes();
485 true
486 }
487
488 fn move_to_new_pane(&mut self, series: SeriesId) -> bool {
490 self.sync_panes();
491 let pane = self.add_pane();
492 let moved = self.move_series_to_pane(series, pane);
493 if moved {
494 let height = self.study_pane_height(series);
495 if let Some(item) = self
496 .pane_layout
497 .panes
498 .iter_mut()
499 .find(|item| item.id == pane)
500 {
501 item.height = height;
502 }
503 } else {
504 self.pane_layout.panes.retain(|item| item.id != pane);
505 }
506 moved
507 }
508
509 pub fn move_indicator_to_pane(&mut self, study: SeriesId, target: SeriesId) -> bool {
514 self.sync_panes();
515 if !self.study_ids.contains(&study) {
516 return false;
517 }
518 if target == study {
519 return self.move_to_new_pane(study);
520 }
521 let pane = if target == SeriesId::MAIN {
522 PaneId::MAIN
523 } else {
524 match self.series_pane(target) {
525 Some(pane) if pane != PaneId::MAIN && self.study_ids.contains(&target) => pane,
526 _ => return false,
527 }
528 };
529 self.move_series_to_pane(study, pane)
530 }
531
532 pub fn move_pane(&mut self, pane: PaneId, index: usize) -> bool {
535 self.sync_panes();
536 let Some(at) = self.pane_index(pane) else {
537 return false;
538 };
539 let item = self.pane_layout.panes.remove(at);
540 let index = index.min(self.pane_layout.panes.len());
541 self.pane_layout.panes.insert(index, item);
542 true
543 }
544
545 pub fn swap_panes(&mut self, first: PaneId, second: PaneId) -> bool {
548 self.sync_panes();
549 let (Some(first), Some(second)) = (self.pane_index(first), self.pane_index(second)) else {
550 return false;
551 };
552 self.pane_layout.panes.swap(first, second);
553 true
554 }
555
556 pub fn pane_height(&self, pane: PaneId) -> Option<f32> {
559 self.pane_layout
560 .panes
561 .iter()
562 .find(|item| item.id == pane)
563 .map(|item| item.rect.height())
564 .filter(|height| *height > 0.0)
565 }
566
567 pub fn set_pane_height(&mut self, pane: PaneId, height: f32) -> bool {
572 self.sync_panes();
573 let Some(at) = self.pane_index(pane) else {
574 return false;
575 };
576 if !(height.is_finite() && height > 0.0) {
577 return false;
578 }
579 self.pane_layout.panes[at].height = Some(height);
580 true
581 }
582
583 pub fn pane_stretch_factor(&self, pane: PaneId) -> Option<f32> {
588 self.pane_index(pane)
589 .map(|at| self.pane_layout.panes[at].stretch)
590 }
591
592 pub fn set_pane_stretch_factor(&mut self, pane: PaneId, factor: f32) -> bool {
595 self.sync_panes();
596 let Some(at) = self.pane_index(pane) else {
597 return false;
598 };
599 if !(factor.is_finite() && factor > 0.0) {
600 return false;
601 }
602 let pane = &mut self.pane_layout.panes[at];
603 pane.stretch = factor;
604 pane.height = None;
605 true
606 }
607
608 pub fn set_preserve_empty_pane(&mut self, pane: PaneId, preserve: bool) -> bool {
611 self.sync_panes();
612 let Some(at) = self.pane_index(pane).filter(|_| pane != PaneId::MAIN) else {
613 return false;
614 };
615 self.pane_layout.panes[at].preserve = preserve;
616 true
617 }
618
619 pub fn preserves_empty_pane(&self, pane: PaneId) -> bool {
621 pane == PaneId::MAIN
622 || self
623 .pane_index(pane)
624 .is_some_and(|at| self.pane_layout.panes[at].preserve)
625 }
626
627 pub fn with_pane_options(mut self, options: PaneOptions) -> Self {
629 self.pane_options = options;
630 self
631 }
632
633 pub fn pane_options(&self) -> &PaneOptions {
635 &self.pane_options
636 }
637
638 pub fn pane_options_mut(&mut self) -> &mut PaneOptions {
640 &mut self.pane_options
641 }
642}
643
644impl Chart {
645 #[cfg(feature = "ui")]
648 pub(crate) fn move_indicator_to_new_pane(&mut self, study: SeriesId, above: bool) -> bool {
649 self.sync_panes();
650 let Some(from) = self
651 .series_pane(study)
652 .and_then(|pane| self.pane_index(pane))
653 else {
654 return false;
655 };
656 let source = &self.pane_layout.panes[from];
657 let kept = source.id == PaneId::MAIN || source.preserve || source.members.len() > 1;
658 if !self.move_indicator_to_pane(study, study) {
659 return false;
660 }
661 let to = from + usize::from(kept && !above);
662 let pane = self
663 .series_pane(study)
664 .expect("study was moved to a new pane");
665 self.move_pane(pane, to)
666 }
667
668 pub fn reorder_indicator(&mut self, id: SeriesId, order: VisualOrder) -> bool {
671 if !self.study_ids.contains(&id) {
672 return false;
673 }
674 self.sync_panes();
675 let layout = &mut self.pane_layout;
676 let Some(items) = std::iter::once(&mut layout.main)
677 .chain(layout.panes.iter_mut().map(|pane| &mut pane.members))
678 .find(|items| items.contains(&id))
679 else {
680 return false;
681 };
682 let at = items.iter().position(|item| *item == id).unwrap();
683 let last = items.len() - 1;
684 let to = match order {
685 VisualOrder::BringToFront => last,
686 VisualOrder::BringForward => (at + 1).min(last),
687 VisualOrder::SendBackward => at.saturating_sub(1),
688 VisualOrder::SendToBack => 0,
689 };
690 let item = items.remove(at);
691 items.insert(to, item);
692 true
693 }
694}
695
696#[derive(Clone, Copy, Debug)]
702#[non_exhaustive]
703pub struct ChartLayoutRects {
704 pub widget_rect: Rect,
706 pub price_rect: Rect,
708 pub volume_rect: Rect,
710 pub legend_rect: Rect,
712}
713
714impl Default for ChartLayoutRects {
715 fn default() -> Self {
716 Self {
717 widget_rect: Rect::NOTHING,
718 price_rect: Rect::NOTHING,
719 volume_rect: Rect::NOTHING,
720 legend_rect: Rect::NOTHING,
721 }
722 }
723}
724
725impl Chart {
726 pub fn calculate_layout_rects(&self, widget_rect: Rect) -> ChartLayoutRects {
736 self.pane_layout_rects(widget_rect, self.config.show_time_labels)
737 }
738
739 pub(in crate::widget) fn pane_layout_rects(
740 &self,
741 widget_rect: Rect,
742 show_time_labels: bool,
743 ) -> ChartLayoutRects {
744 let padding = self
745 .config
746 .padding
747 .max(0.0)
748 .min(widget_rect.width().min(widget_rect.height()).max(0.0) / 2.0);
749 let study_axes = if self.config.show_left_axis || self.config.show_right_axis {
750 self.studies
751 .indicators()
752 .iter()
753 .filter(|study| {
754 study.is_visible()
755 && study.is_overlay()
756 && !study.is_volume()
757 && study.scale() == crate::studies::StudyScale::Independent
758 })
759 .count() as f32
760 * self
761 .measured_axis_width
762 .max(self.config.right_axis_width)
763 .max(self.config.left_axis_width)
764 } else {
765 0.0
766 };
767 let series_axes = self.series_axis_keys();
768 let series_left = series_axes
769 .iter()
770 .filter(|key| **key != crate::model::SeriesScale::Right)
771 .count() as f32
772 * self
773 .measured_axis_width
774 .max(self.config.right_axis_width)
775 .max(self.config.left_axis_width);
776 let series_right = if series_axes.contains(&crate::model::SeriesScale::Right) {
777 self.measured_axis_width.max(self.config.right_axis_width)
778 } else {
779 0.0
780 };
781 let study_axes = study_axes + series_left;
782 let left_axis = if self.config.show_left_axis {
783 self.config.left_axis_width.max(self.measured_axis_width)
784 } else {
785 0.0
786 };
787 let right_axis = if self.config.show_right_axis {
788 self.config.right_axis_width.max(self.measured_axis_width)
789 } else {
790 0.0
791 } + series_right;
792 let study_axes = study_axes
793 .min((widget_rect.width() - 2.0 * padding - left_axis - right_axis).max(0.0) * 0.5);
794 let left_axis = left_axis + study_axes;
795 let bottom_padding = if show_time_labels {
796 self.time_axis_height()
797 } else {
798 0.0
799 };
800 let price_rect = Rect::from_min_size(
801 widget_rect.min + Vec2::new((padding + left_axis).min(widget_rect.width()), padding),
802 Vec2::new(
803 (widget_rect.width() - 2.0 * padding - left_axis - right_axis).max(0.0),
804 (widget_rect.height() - 2.0 * padding - bottom_padding).max(0.0),
805 ),
806 );
807 let legend_rect = if self.config.show_ohlc_info {
808 price_rect.shrink(8.0).intersect(Rect::from_min_size(
809 price_rect.min + Vec2::splat(8.0),
810 Vec2::new(price_rect.width(), 24.0),
811 ))
812 } else {
813 Rect::NOTHING
814 };
815 let volume_rect = if self.shows_volume() {
816 volume::overlay_rect(price_rect, &self.config)
817 } else {
818 Rect::ZERO
819 };
820
821 ChartLayoutRects {
822 widget_rect,
823 price_rect,
824 volume_rect,
825 legend_rect,
826 }
827 }
828}
829
830#[cfg(all(test, feature = "studies"))]
831mod tests {
832 use crate::widget::test_frames::chart;
833 use egui::{Context, Pos2, RawInput, Rect, Vec2};
834
835 #[test]
838 fn host_panes_take_series_share_height_by_stretch_and_reorder() {
839 use crate::widget::{PaneId, SeriesId, SeriesOptions};
840 let ctx = Context::default();
841 let mut chart = chart(&[100.0], &[110.0]);
842 let series = chart.add_series(SeriesOptions::default());
843 let bars = (chart.bars(SeriesId::MAIN).iter().enumerate())
844 .map(|(i, bar)| {
845 let close = 1000.0 + i as f64;
846 crate::model::Bar::new(bar.time, close, close, close, close, 1.0)
847 })
848 .collect();
849 chart.set_data(series, bars);
850 let pane = chart.add_pane();
851 assert!(chart.move_series_to_pane(series, pane));
852 assert!(chart.set_pane_stretch_factor(pane, 3.0));
853 assert!(chart.move_pane(pane, 0));
854 assert_eq!(chart.panes(), [pane, PaneId::MAIN]);
855 assert_eq!(chart.pane_series(pane), [series]);
856 assert_eq!(chart.pane_series(PaneId::MAIN), [SeriesId::MAIN]);
857 let show = |chart: &mut crate::widget::Chart, height: f32| {
858 ctx.run_ui(
859 RawInput {
860 screen_rect: Some(Rect::from_min_size(Pos2::ZERO, Vec2::new(800.0, height))),
861 ..Default::default()
862 },
863 |ui| {
864 egui::CentralPanel::default().show(ui, |ui| chart.show(ui));
865 },
866 )
867 .drop_without_applying_deltas();
868 };
869 show(&mut chart, 600.0);
870 let heights = |chart: &crate::widget::Chart| {
871 (
872 chart.pane_height(pane).unwrap(),
873 chart.pane_height(PaneId::MAIN).unwrap(),
874 )
875 };
876 let (top, main) = heights(&chart);
877 assert!((top - main).abs() < 0.01, "equal factors share equally");
878 assert!(chart.pane_layout.panes[0].rect.bottom() <= chart.get_rendered_price_rect().top());
879 let range = chart.pane_layout.panes[0].scale.range();
880 assert!(range.min < 1000.0 && range.max > 1099.0);
881 assert!(chart.get_chart_mapping().price.range().max < 1000.0);
882 assert!(chart.set_pane_height(pane, 200.0));
883 show(&mut chart, 600.0);
884 let (top, main) = heights(&chart);
885 assert!((top - 200.0).abs() < 0.01);
886 let share = top / (top + main);
887 show(&mut chart, 400.0);
888 let (top, main) = heights(&chart);
889 assert!(
890 (top / (top + main) - share).abs() < 1e-4,
891 "a resize keeps the shares"
892 );
893 assert!(chart.set_pane_stretch_factor(pane, 3e38));
895 show(&mut chart, 600.0);
896 let (top, main) = heights(&chart);
897 assert!(top.is_finite() && top > main, "{top} {main}");
898 assert!(chart.set_preserve_empty_pane(pane, true));
899 assert!(chart.move_series_to_pane(series, PaneId::MAIN));
900 assert_eq!(chart.panes(), [pane, PaneId::MAIN]);
901 assert!(chart.set_preserve_empty_pane(pane, false));
902 assert!(chart.move_series_to_pane(series, pane));
903 assert!(chart.move_series_to_pane(series, PaneId::MAIN));
904 assert_eq!(chart.panes(), [PaneId::MAIN]);
905 }
906
907 #[test]
908 fn pane_defaults_do_not_depend_on_the_study_name() {
909 let ctx = Context::default();
910 let mut chart = chart(&[100.0], &[110.0]);
911 chart.add_indicator(Box::new(
912 crate::studies::CustomIndicator::new("RSI", |bars| {
913 bars.iter()
914 .map(|_| crate::studies::IndicatorValue::Single(200.0))
915 .collect()
916 })
917 .with_overlay(false),
918 ));
919 crate::widget::test_frames::frame(&ctx, &mut chart, 0.0, Vec::new());
920 let pane = &chart.get_rendered_indicator_panes()[0];
921 assert!((pane.y_min - 199.8).abs() < 1e-9);
923 assert!((pane.y_max - 200.2).abs() < 1e-9);
924 }
925}