1use super::Chart;
4use crate::drawings::persistence::{
5 DrawingStorage, LoadError, SaveError, SkipReason, SkippedDrawing, StoredCoord, StoredDrawing,
6 drawing_to_stored_with, stored_to_drawing_with,
7};
8use crate::drawings::{ChartPoint, Drawing, DrawingManager};
9use crate::model::{index_at_with_timeframe, time_at_with_timeframe};
10
11impl Chart {
12 pub fn drawing_point_time(&self, point: &ChartPoint) -> Option<chrono::DateTime<chrono::Utc>> {
15 point
16 .time_anchor
17 .filter(|(_, index)| *index == point.bar_idx)
18 .map(|(time, _)| time)
19 .or_else(|| self.index_to_time(point.bar_idx as f64))
20 }
21
22 pub fn drawing_point_at(
25 &self,
26 time: chrono::DateTime<chrono::Utc>,
27 price: f64,
28 ) -> Option<ChartPoint> {
29 let index = self.time_to_index(time)? as f32;
30 if !index.is_finite() || !price.is_finite() {
31 return None;
32 }
33 let mut point = ChartPoint::new(index, price);
34 point.time_anchor = Some((time, index));
35 Some(point)
36 }
37
38 pub(super) fn anchor_drawing_points(&mut self) {
40 let bars = self.state.shown();
41 let timeframe = self.timeframe();
42 let timezone = self.calendar_timezone();
43 let mut anchor = |point: &mut ChartPoint| {
44 if point
45 .time_anchor
46 .is_none_or(|(_, projected)| projected != point.bar_idx)
47 {
48 point.time_anchor = time_at_with_timeframe(
49 bars,
50 point.bar_idx as f64,
51 timeframe,
52 timezone.as_ref(),
53 )
54 .map(|time| (time, point.bar_idx));
55 }
56 };
57 self.drawings.visit_chart_points(&mut anchor);
58 self.history.visit_chart_points(&mut anchor);
59 }
60
61 pub(super) fn project_drawing_points(&mut self) {
63 let bars = self.state.shown();
64 let timeframe = self.timeframe();
65 let timezone = self.calendar_timezone();
66 let state = &self.state;
67 let chart_type = self.chart_type;
68 let params = &self.chart_type_params;
69 let older_bars_drawn = self.older_bars_drawn;
70 let mut project = |point: &mut ChartPoint| {
71 if let Some((time, _)) = point.time_anchor {
72 let index = crate::chart::rendering::candles::price_based::element_index_at(
73 state,
74 chart_type,
75 params,
76 older_bars_drawn,
77 time,
78 )
79 .map(|index| index as f64)
80 .or_else(|| index_at_with_timeframe(bars, time, timeframe, timezone.as_ref()));
81 if let Some(index) = index {
82 if time_at_with_timeframe(
84 bars,
85 point.bar_idx as f64,
86 timeframe,
87 timezone.as_ref(),
88 ) != Some(time)
89 {
90 point.bar_idx = index as f32;
91 point.time_anchor = Some((time, point.bar_idx));
92 }
93 }
94 }
95 };
96 self.drawings.visit_chart_points(&mut project);
97 self.history.visit_chart_points(&mut project);
98 }
99
100 pub fn save_drawings(&self) -> Result<DrawingStorage, SaveError> {
108 self.store_drawings(
109 self.drawings
110 .drawings
111 .iter()
112 .filter(|drawing| drawing.saveable),
113 )
114 }
115
116 pub(super) fn store_drawings<'a>(
117 &self,
118 drawings: impl IntoIterator<Item = &'a Drawing>,
119 ) -> Result<DrawingStorage, SaveError> {
120 let mut storage = DrawingStorage::new(self.symbol());
121 storage.timeframe = self
122 .timeframe()
123 .map(|timeframe| timeframe.as_str().into_owned());
124 for drawing in drawings {
125 for (at, point) in drawing.chart_points.iter().enumerate() {
126 if point
127 .time_anchor
128 .is_none_or(|(_, index)| index != point.bar_idx)
129 && self.index_to_time(point.bar_idx as f64).is_none()
130 {
131 return Err(SaveError::UnmappedAnchor {
132 id: drawing
133 .stored_id
134 .clone()
135 .unwrap_or_else(|| drawing.id.to_string()),
136 point: at,
137 });
138 }
139 }
140 let mut stored = drawing_to_stored_with(drawing, |index, price| {
141 StoredCoord::from_time(
142 self.index_to_time(index as f64).expect("validated anchor"),
143 price,
144 )
145 });
146 stored.pane = self
147 .drawing_pane(drawing.id)
148 .and_then(|pane| self.stored_pane(pane));
149 storage.add_drawing(stored);
150 }
151 storage.to_json()?;
153 Ok(storage)
154 }
155
156 pub fn load_drawings(&mut self, json: &str) -> Result<Vec<SkippedDrawing>, LoadError> {
166 self.load_drawings_remapped(json, false)
167 }
168
169 pub(super) fn load_drawings_remapped(
173 &mut self,
174 json: &str,
175 remap: bool,
176 ) -> Result<Vec<SkippedDrawing>, LoadError> {
177 let (storage, mut skipped) = DrawingStorage::from_json(json)?;
178 if storage.symbol != self.symbol() {
179 return Err(LoadError::OtherSymbol);
180 }
181 let mut manager = DrawingManager::new();
182 manager.set_sync_enabled(self.drawings.sync_enabled);
183 manager.registry = self.drawings.registry.clone();
184 manager.options = self.drawings.options.clone();
185 manager.tool_defaults = self.drawings.tool_defaults.clone();
186 manager.active_tool = self.drawings.active_tool;
187 if let Some(id) = self.drawings.active_custom_tool() {
188 let _ = manager.set_custom_tool(id);
189 }
190 manager.stay_in_drawing_mode = self.drawings.stay_in_drawing_mode;
191 manager.eraser_mode = self.drawings.eraser_mode;
192 manager.curr_timeframe = self.drawings.curr_timeframe.clone();
193 let mut refused = Vec::new();
194 let read = (0..).filter(|index| skipped.iter().all(|s| s.index != *index));
196 for (index, stored) in read.zip(storage.drawings()) {
197 if let Err(error) = manager.add_synced_drawing(self.restored_drawing(stored, remap)) {
198 refused.push(SkippedDrawing {
199 index,
200 id: Some(stored.id.clone()),
201 error: SkipReason::Invalid(serde::de::Error::custom(error)),
202 });
203 }
204 }
205 skipped.extend(refused);
206 skipped.sort_by_key(|skipped| skipped.index);
207 self.drawings = manager;
208 self.history.clear();
209 Ok(skipped)
210 }
211
212 pub(super) fn restored_drawing(&self, stored: &StoredDrawing, remap: bool) -> Drawing {
216 let mut drawing = stored_to_drawing_with(
217 stored,
218 |point| {
219 point
220 .time()
221 .and_then(|time| self.time_to_index(time))
222 .unwrap_or(0.0) as f32
223 },
224 0,
225 );
226 if let Some(id) = drawing
227 .pane
228 .filter(|_| remap)
229 .and_then(|pane| self.restored_studies.get(&pane))
230 {
231 drawing.pane = Some(*id);
232 } else if let Some(pane) = &stored.pane {
233 drawing.pane = self.stored_pane_owner(pane);
234 }
235 drawing
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use crate::drawings::{Drawing, DrawingToolType};
243 use crate::model::{Bar, BarData};
244 use crate::widget::SeriesId;
245 use chrono::{TimeZone, Utc};
246
247 #[test]
248 fn whitespace_anchors_keep_their_slots_and_axis_dates() {
249 use crate::model::Timeframe;
250 use chrono::Duration;
251
252 let date = |y, m, d| Utc.with_ymd_and_hms(y, m, d, 0, 0, 0).unwrap();
253 let hourly = [
254 (4, 9, 30),
255 (4, 10, 0),
256 (4, 11, 0),
257 (4, 12, 0),
258 (4, 13, 0),
259 (4, 14, 0),
260 (4, 15, 0),
261 (5, 9, 30),
262 (5, 10, 0),
263 (5, 11, 0),
264 ]
265 .map(|(day, hour, minute)| Utc.with_ymd_and_hms(2024, 3, day, hour, minute, 0).unwrap())
266 .to_vec();
267 let bar = |time| Bar::new(time, 10.0, 12.0, 9.0, 11.0, 1.0);
268 for (times, timeframe, timezone, expected_left, expected_right, next) in [
269 (
271 vec![date(2024, 2, 8), date(2024, 2, 9), date(2024, 2, 12)],
272 None,
273 "UTC",
274 date(2024, 2, 7),
275 date(2024, 2, 13),
276 date(2024, 2, 13),
277 ),
278 (
280 vec![date(2024, 2, 2), date(2024, 2, 5), date(2024, 2, 6)],
281 None,
282 "UTC",
283 date(2024, 2, 1),
284 date(2024, 2, 7),
285 date(2024, 2, 7),
286 ),
287 (
288 vec![
289 date(2024, 1, 2) + Duration::minutes(1258),
290 date(2024, 1, 2) + Duration::minutes(1259),
291 date(2024, 1, 3) + Duration::minutes(870),
292 ],
293 Some(Timeframe::Min1),
294 "UTC",
295 date(2024, 1, 2) + Duration::minutes(1257),
296 date(2024, 1, 3) + Duration::minutes(871),
297 date(2024, 1, 3) + Duration::minutes(871),
298 ),
299 (
300 vec![date(2024, 2, 1), date(2024, 3, 1), date(2024, 4, 1)],
301 Some(Timeframe::Month1),
302 "UTC",
303 date(2024, 1, 1),
304 date(2024, 5, 1),
305 date(2024, 5, 1),
306 ),
307 (
308 vec![date(2024, 7, 1), date(2024, 10, 1), date(2025, 1, 1)],
309 Some("3month".parse().unwrap()),
310 "UTC",
311 date(2024, 4, 1),
312 date(2025, 4, 1),
313 date(2025, 4, 1),
314 ),
315 (
316 vec![date(2023, 12, 31), date(2024, 1, 31), date(2024, 2, 29)],
317 Some(Timeframe::Month1),
318 "UTC",
319 date(2023, 11, 30),
320 date(2024, 3, 31),
321 date(2024, 3, 31),
322 ),
323 (
324 vec![date(2023, 1, 1), date(2024, 1, 1), date(2025, 1, 1)],
325 Some("12month".parse().unwrap()),
326 "UTC",
327 date(2022, 1, 1),
328 date(2026, 1, 1),
329 date(2026, 1, 1),
330 ),
331 #[cfg(feature = "timezones")]
333 (
334 vec![
335 date(2024, 9, 1) + Duration::hours(4),
336 date(2024, 10, 1) + Duration::hours(4),
337 date(2024, 11, 1) + Duration::hours(4),
338 ],
339 Some(Timeframe::Month1),
340 "America/New_York",
341 date(2024, 8, 1) + Duration::hours(4),
342 date(2024, 12, 1) + Duration::hours(5),
343 date(2024, 12, 1) + Duration::hours(5),
344 ),
345 #[cfg(feature = "timezones")]
347 (
348 vec![date(2024, 1, 1), date(2024, 2, 1), date(2024, 3, 1)],
349 Some(Timeframe::Month1),
350 "America/New_York",
351 date(2023, 12, 1),
352 date(2024, 4, 1),
353 date(2024, 4, 1),
354 ),
355 (
357 hourly[..8].to_vec(),
358 Some(Timeframe::Hour1),
359 "UTC",
360 Utc.with_ymd_and_hms(2024, 3, 4, 9, 0, 0).unwrap(),
361 Utc.with_ymd_and_hms(2024, 3, 5, 10, 0, 0).unwrap(),
362 Utc.with_ymd_and_hms(2024, 3, 5, 10, 0, 0).unwrap(),
363 ),
364 (
365 hourly[..8].to_vec(),
366 None,
367 "UTC",
368 Utc.with_ymd_and_hms(2024, 3, 4, 9, 0, 0).unwrap(),
369 Utc.with_ymd_and_hms(2024, 3, 5, 10, 0, 0).unwrap(),
370 Utc.with_ymd_and_hms(2024, 3, 5, 10, 0, 0).unwrap(),
371 ),
372 (
374 hourly.clone(),
375 Some(Timeframe::Hour1),
376 "UTC",
377 Utc.with_ymd_and_hms(2024, 3, 4, 9, 0, 0).unwrap(),
378 Utc.with_ymd_and_hms(2024, 3, 5, 12, 0, 0).unwrap(),
379 Utc.with_ymd_and_hms(2024, 3, 5, 12, 0, 0).unwrap(),
380 ),
381 (
382 hourly,
383 None,
384 "UTC",
385 Utc.with_ymd_and_hms(2024, 3, 4, 9, 0, 0).unwrap(),
386 Utc.with_ymd_and_hms(2024, 3, 5, 12, 0, 0).unwrap(),
387 Utc.with_ymd_and_hms(2024, 3, 5, 12, 0, 0).unwrap(),
388 ),
389 (
391 vec![date(2024, 1, 8), date(2024, 1, 16), date(2024, 1, 22)],
392 Some(Timeframe::Week1),
393 "UTC",
394 date(2024, 1, 1),
395 date(2024, 1, 29),
396 date(2024, 1, 29),
397 ),
398 (
400 [0, 250, 500]
401 .map(|ms| date(2024, 3, 4) + Duration::milliseconds(ms))
402 .to_vec(),
403 Some(Timeframe::Ms250),
404 "UTC",
405 date(2024, 3, 4) - Duration::milliseconds(250),
406 date(2024, 3, 4) + Duration::milliseconds(750),
407 date(2024, 3, 4) + Duration::milliseconds(750),
408 ),
409 ] {
410 let last = times.len() - 1;
411 let right_index = (last + 1) as f32;
412 let mut chart = Chart::new(BarData::from_bars(times.into_iter().map(bar).collect()));
413 if let Some(timeframe) = timeframe {
414 chart.set_timeframe(timeframe);
415 }
416 chart.chart_options.time_scale.timezone =
417 crate::scales::TimezoneMode::Timezone(timezone.parse().unwrap());
418 let mut drawing = Drawing::new(7, DrawingToolType::TrendLine);
419 drawing.chart_points = vec![
420 ChartPoint::new(-1.0, 10.0),
421 ChartPoint::new(right_index, 11.0),
422 ];
423 drawing.completed = true;
424 chart.drawings_mut().drawings.push(drawing);
425 for (index, expected) in [(right_index, expected_right), (-1.0, expected_left)] {
426 assert_eq!(
427 chart.drawing_point_time(&ChartPoint::new(index, 10.0)),
428 Some(expected),
429 "{timeframe:?}"
430 );
431 assert_eq!(
432 chart.drawing_point_at(expected, 10.0).unwrap().bar_idx,
433 index
434 );
435 assert_eq!(chart.index_to_time(index as f64), Some(expected));
436 }
437 for (index, left, right) in [
438 (-0.5, expected_left, chart.state.shown()[0].time),
439 (
440 last as f64 + 0.5,
441 chart.state.shown()[last].time,
442 expected_right,
443 ),
444 ] {
445 let midpoint = left + (right - left) / 2;
446 assert_eq!(chart.index_to_time(index), Some(midpoint));
447 assert_eq!(chart.time_to_index(midpoint), Some(index));
448 }
449 let ctx = egui::Context::default();
451 let painter =
452 egui::Painter::new(ctx, egui::LayerId::background(), egui::Rect::EVERYTHING);
453 let rect = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(2000.0, 300.0));
455 let price = crate::scales::PriceMap::new(
456 crate::scales::PriceTransform::default(),
457 crate::scales::PriceRange::new(9.0, 12.0),
458 0.0,
459 300.0,
460 false,
461 );
462 for start in [0, last] {
463 let coords = crate::chart::rendering::ChartMapping::new(
464 rect, 100.0, start, last, 5.0, price,
465 );
466 let (_, keys, axis) = crate::chart::rendering::time_axis_marks(
467 &crate::chart::rendering::RenderContext::new(&painter, rect),
468 &chart.state.shown()[start..],
469 &coords,
470 &chart.build_time_formatter(),
471 &chart.chart_options.time_scale,
472 chart.calendar_timezone().as_ref(),
473 |index| chart.index_to_time(index),
474 );
475 let shift = (axis.base_idx - coords.base_idx) as f32;
477 for (index, expected) in [(-1.0, expected_left), (right_index, expected_right)] {
478 assert_eq!(
479 keys.iter()
480 .find(|(_, key)| *key as f32 - shift == index)
481 .map(|(time, _)| *time),
482 Some(expected),
483 "{timeframe:?}, key {index}"
484 );
485 }
486 }
487 let saved = chart.save_drawings().unwrap().to_json().unwrap();
488 chart.load_drawings(&saved).unwrap();
489 chart.update(SeriesId::MAIN, bar(next));
490 assert_eq!(
491 chart.drawings().drawings[0].chart_points[1].bar_idx,
492 right_index
493 );
494 let mut extended = vec![bar(expected_left)];
495 extended.extend_from_slice(&chart.state.data().bars);
496 chart.set_data(SeriesId::MAIN, extended);
497 assert_eq!(chart.drawings().drawings[0].chart_points[0].bar_idx, 0.0);
498 assert_eq!(
499 chart.drawings().drawings[0].chart_points[1].bar_idx,
500 right_index + 1.0
501 );
502 }
503 }
504
505 #[test]
507 fn drawing_storage_preserves_submillisecond_anchors() {
508 for start in [
509 Utc.timestamp_opt(1_704_067_200, 0).unwrap(),
510 Utc.timestamp_opt(-1, 999_999_999).unwrap(),
511 ] {
512 let bars = crate::data::aggregate_trades(
513 &(0..3)
514 .map(|id| crate::model::Tick::new(start, 10.0, 1.0, None).with_id(id))
515 .collect::<Vec<_>>(),
516 "1t".parse::<crate::model::Timeframe>().unwrap(),
517 )
518 .unwrap();
519 let mut chart = Chart::new(BarData::from_bars(bars.clone()));
520 let mut drawing = Drawing::new(7, DrawingToolType::TrendLine);
521 drawing.chart_points = vec![ChartPoint::new(1.0, 10.0), ChartPoint::new(2.0, 11.0)];
522 drawing.completed = true;
523 chart.drawings_mut().drawings.push(drawing);
524 let saved = chart.save_drawings().unwrap().to_json().unwrap();
525 chart.load_drawings(&saved).unwrap();
526 assert_eq!(
527 chart.drawings().drawings[0]
528 .chart_points
529 .iter()
530 .map(|p| p.bar_idx)
531 .collect::<Vec<_>>(),
532 [1.0, 2.0]
533 );
534 assert_eq!(
535 chart.drawing_point_time(&chart.drawings().drawings[0].chart_points[1]),
536 Some(start + chrono::Duration::nanoseconds(2))
537 );
538 let id = chart.drawings().drawings[0].id;
539 chart.drawings_mut().select(id);
540 let copied = chart.copy_selected_drawings().unwrap().unwrap();
541 chart.paste_drawings(&copied).unwrap();
542 assert_eq!(
543 chart
544 .drawings()
545 .drawings
546 .last()
547 .unwrap()
548 .chart_points
549 .iter()
550 .map(|point| point.bar_idx)
551 .collect::<Vec<_>>(),
552 [4.0, 5.0]
553 );
554 assert!(chart.undo());
555 chart.set_data(SeriesId::MAIN, Vec::new());
557 let saved = chart.save_drawings().unwrap().to_json().unwrap();
558 chart.load_drawings(&saved).unwrap();
559 chart.set_data(SeriesId::MAIN, bars);
560 assert_eq!(
561 chart.drawings().drawings[0]
562 .chart_points
563 .iter()
564 .map(|p| p.bar_idx)
565 .collect::<Vec<_>>(),
566 [1.0, 2.0]
567 );
568 }
569 }
570
571 #[test]
572 fn saved_drawings_restore_before_bars_and_report_skipped_records() {
573 let bars = || {
574 [0, 60, 180]
575 .map(|seconds| {
576 Bar::new(
577 Utc.timestamp_opt(seconds, 0).unwrap(),
578 10.0,
579 11.0,
580 9.0,
581 10.0,
582 1.0,
583 )
584 })
585 .to_vec()
586 };
587 let mut chart = Chart::new(BarData::from_bars(bars()));
588 chart.set_symbol("A");
589 let mut drawing = Drawing::new(7, DrawingToolType::TrendLine);
590 drawing.chart_points = [-0.5, 1.5, 3.5]
591 .map(|index| ChartPoint::new(index, 10.0))
592 .to_vec();
593 drawing.completed = true;
594 chart.drawings_mut().drawings.push(drawing);
595 let saved = chart.save_drawings().unwrap();
596 chart.drawings_mut().select(7);
597 let mut unrelated = chart.drawings().drawings[0].clone();
598 unrelated.id = 8;
599 unrelated.font_size = f32::NAN;
600 chart.drawings_mut().drawings.push(unrelated);
601 assert!(chart.save_drawings().is_err());
602 let copied = chart.copy_selected_drawings().unwrap().unwrap();
603 chart.drawings_mut().drawings.pop();
604 chart.paste_drawings(&copied).unwrap();
605 assert_eq!(chart.drawings().drawings.len(), 2);
606 assert!(chart.undo());
607 assert_eq!(chart.drawings().drawings.len(), 1);
608 assert_eq!(
610 saved.drawings[0]
611 .points
612 .iter()
613 .map(|p| p.ts_ms)
614 .collect::<Vec<_>>(),
615 [-30_000, 120_000, 270_000]
616 );
617 let mut json = serde_json::to_value(&saved).unwrap();
618 let valid = json["drawings"][0].clone();
619 let mut bad_time = valid.clone();
620 bad_time["id"] = "bad-time".into();
621 bad_time["points"][0]["ts_ms"] = i64::MAX.into();
622 json["drawings"]
623 .as_array_mut()
624 .unwrap()
625 .extend([serde_json::json!({"id": "broken"}), bad_time]);
626 let mut restored = Chart::new(BarData::new());
627 restored.set_symbol("A");
628 restored.drawings_mut().stay_in_drawing_mode = true;
629 restored.drawings_mut().eraser_mode = true;
630 let skipped = restored.load_drawings(&json.to_string()).unwrap();
631 assert_eq!(skipped.iter().map(|s| s.index).collect::<Vec<_>>(), [1, 2]);
632 assert!(restored.drawings().pending_sync.lock().unwrap().is_empty());
633 assert!(!restored.can_undo());
634 assert!(restored.drawings().stay_in_drawing_mode);
635 assert!(restored.drawings().eraser_mode);
636 assert_eq!(
638 serde_json::to_value(&restored.save_drawings().unwrap().drawings[0].points).unwrap(),
639 serde_json::to_value(&saved.drawings[0].points).unwrap()
640 );
641 restored.set_data(SeriesId::MAIN, bars());
642 assert_eq!(
643 restored.drawings().drawings[0].chart_points,
644 chart.drawings().drawings[0].chart_points
645 );
646 json["symbol"] = "B".into();
647 assert!(matches!(
648 restored.load_drawings(&json.to_string()),
649 Err(LoadError::OtherSymbol)
650 ));
651 assert_eq!(restored.drawings().drawings.len(), 1);
652 let mut empty = Chart::new(BarData::new());
653 empty.drawings_mut().drawings = chart.drawings().drawings.clone();
654 assert!(empty.save_drawings().is_err());
655 }
656
657 #[test]
660 fn a_refused_paste_reports_why() {
661 use crate::drawings::persistence::{LoadError, SkipReason};
662 use crate::widget::test_frames;
663 let mut source = test_frames::chart(&[100.0], &[110.0]);
664 source.set_symbol("A");
665 let mut line = Drawing::new(0, DrawingToolType::TrendLine);
666 line.chart_points = vec![ChartPoint::new(1.0, 105.0), ChartPoint::new(2.0, 106.0)];
667 let id = source.drawings_mut().add_drawing(line).unwrap();
668 source.drawings_mut().select(id);
669 let copied = source.copy_selected_drawings().unwrap().unwrap();
670 let newer = copied.replace("\"TrendLine\"", "\"NewerTool\"");
671 type Refused = fn(&LoadError) -> bool;
672 let cases: [(&str, &str, bool, Refused); 3] = [
673 ("hello", "A", true, |e| matches!(e, LoadError::NotClipboard)),
674 (&newer, "A", true, |e| {
675 matches!(e, LoadError::Skipped(s) if s.len() == 1
676 && matches!(&s[0].error, SkipReason::UnknownTool(t) if t.input == "NewerTool"))
677 }),
678 (&copied, "A", false, |e| {
679 matches!(e, LoadError::NeedsHistory)
680 }),
681 ];
682 for (text, symbol, bars, refused) in cases {
683 let mut target = if bars {
684 test_frames::chart(&[100.0], &[110.0])
685 } else {
686 Chart::new(BarData::new())
687 };
688 target.set_symbol(symbol);
689 let error = target.paste_drawings(text).unwrap_err();
690 assert!(refused(&error), "{symbol}: {error:?}");
691 assert!(target.drawings().drawings.is_empty(), "{error}");
692 }
693 }
694}