tui_temp_fork/widgets/
chart.rs

1use std::cmp::max;
2
3use unicode_width::UnicodeWidthStr;
4
5use crate::buffer::Buffer;
6use crate::layout::Rect;
7use crate::style::Style;
8use crate::symbols;
9use crate::widgets::canvas::{Canvas, Points};
10use crate::widgets::{Block, Borders, Widget};
11
12/// An X or Y axis for the chart widget
13pub struct Axis<'a, L>
14where
15	L: AsRef<str> + 'a,
16{
17	/// Title displayed next to axis end
18	title: Option<&'a str>,
19	/// Style of the title
20	title_style: Style,
21	/// Bounds for the axis (all data points outside these limits will not be represented)
22	bounds: [f64; 2],
23	/// A list of labels to put to the left or below the axis
24	labels: Option<&'a [L]>,
25	/// The labels' style
26	labels_style: Style,
27	/// The style used to draw the axis itself
28	style: Style,
29}
30
31impl<'a, L> Default for Axis<'a, L>
32where
33	L: AsRef<str>,
34{
35	fn default() -> Axis<'a, L> {
36		Axis {
37			title: None,
38			title_style: Default::default(),
39			bounds: [0.0, 0.0],
40			labels: None,
41			labels_style: Default::default(),
42			style: Default::default(),
43		}
44	}
45}
46
47impl<'a, L> Axis<'a, L>
48where
49	L: AsRef<str>,
50{
51	pub fn title(mut self, title: &'a str) -> Axis<'a, L> {
52		self.title = Some(title);
53		self
54	}
55
56	pub fn title_style(mut self, style: Style) -> Axis<'a, L> {
57		self.title_style = style;
58		self
59	}
60
61	pub fn bounds(mut self, bounds: [f64; 2]) -> Axis<'a, L> {
62		self.bounds = bounds;
63		self
64	}
65
66	pub fn labels(mut self, labels: &'a [L]) -> Axis<'a, L> {
67		self.labels = Some(labels);
68		self
69	}
70
71	pub fn labels_style(mut self, style: Style) -> Axis<'a, L> {
72		self.labels_style = style;
73		self
74	}
75
76	pub fn style(mut self, style: Style) -> Axis<'a, L> {
77		self.style = style;
78		self
79	}
80}
81
82/// Marker to use when plotting data points
83pub enum Marker {
84	/// One point per cell
85	Dot,
86	/// Up to 8 points per cell
87	Braille,
88}
89
90/// A group of data points
91pub struct Dataset<'a> {
92	/// Name of the dataset (used in the legend if shown)
93	name: &'a str,
94	/// A reference to the actual data
95	data: &'a [(f64, f64)],
96	/// Symbol used for each points of this dataset
97	marker: Marker,
98	/// Style used to plot this dataset
99	style: Style,
100}
101
102impl<'a> Default for Dataset<'a> {
103	fn default() -> Dataset<'a> {
104		Dataset {
105			name: "",
106			data: &[],
107			marker: Marker::Dot,
108			style: Style::default(),
109		}
110	}
111}
112
113impl<'a> Dataset<'a> {
114	pub fn name(mut self, name: &'a str) -> Dataset<'a> {
115		self.name = name;
116		self
117	}
118
119	pub fn data(mut self, data: &'a [(f64, f64)]) -> Dataset<'a> {
120		self.data = data;
121		self
122	}
123
124	pub fn marker(mut self, marker: Marker) -> Dataset<'a> {
125		self.marker = marker;
126		self
127	}
128
129	pub fn style(mut self, style: Style) -> Dataset<'a> {
130		self.style = style;
131		self
132	}
133}
134
135/// A container that holds all the infos about where to display each elements of the chart (axis,
136/// labels, legend, ...).
137#[derive(Debug)]
138struct ChartLayout {
139	title_x: Option<(u16, u16)>,
140	title_y: Option<(u16, u16)>,
141	label_x: Option<u16>,
142	label_y: Option<u16>,
143	axis_x: Option<u16>,
144	axis_y: Option<u16>,
145	legend_area: Option<Rect>,
146	graph_area: Rect,
147}
148
149impl Default for ChartLayout {
150	fn default() -> ChartLayout {
151		ChartLayout {
152			title_x: None,
153			title_y: None,
154			label_x: None,
155			label_y: None,
156			axis_x: None,
157			axis_y: None,
158			legend_area: None,
159			graph_area: Rect::default(),
160		}
161	}
162}
163
164/// A widget to plot one or more dataset in a cartesian coordinate system
165///
166/// # Examples
167///
168/// ```
169/// # use tui_temp_fork::widgets::{Block, Borders, Chart, Axis, Dataset, Marker};
170/// # use tui_temp_fork::style::{Style, Color};
171/// # fn main() {
172/// Chart::default()
173///     .block(Block::default().title("Chart"))
174///     .x_axis(Axis::default()
175///         .title("X Axis")
176///         .title_style(Style::default().fg(Color::Red))
177///         .style(Style::default().fg(Color::White))
178///         .bounds([0.0, 10.0])
179///         .labels(&["0.0", "5.0", "10.0"]))
180///     .y_axis(Axis::default()
181///         .title("Y Axis")
182///         .title_style(Style::default().fg(Color::Red))
183///         .style(Style::default().fg(Color::White))
184///         .bounds([0.0, 10.0])
185///         .labels(&["0.0", "5.0", "10.0"]))
186///     .datasets(&[Dataset::default()
187///                     .name("data1")
188///                     .marker(Marker::Dot)
189///                     .style(Style::default().fg(Color::Cyan))
190///                     .data(&[(0.0, 5.0), (1.0, 6.0), (1.5, 6.434)]),
191///                 Dataset::default()
192///                     .name("data2")
193///                     .marker(Marker::Braille)
194///                     .style(Style::default().fg(Color::Magenta))
195///                     .data(&[(4.0, 5.0), (5.0, 8.0), (7.66, 13.5)])]);
196/// # }
197/// ```
198pub struct Chart<'a, LX, LY>
199where
200	LX: AsRef<str> + 'a,
201	LY: AsRef<str> + 'a,
202{
203	/// A block to display around the widget eventually
204	block: Option<Block<'a>>,
205	/// The horizontal axis
206	x_axis: Axis<'a, LX>,
207	/// The vertical axis
208	y_axis: Axis<'a, LY>,
209	/// A reference to the datasets
210	datasets: &'a [Dataset<'a>],
211	/// The widget base style
212	style: Style,
213}
214
215impl<'a, LX, LY> Default for Chart<'a, LX, LY>
216where
217	LX: AsRef<str>,
218	LY: AsRef<str>,
219{
220	fn default() -> Chart<'a, LX, LY> {
221		Chart {
222			block: None,
223			x_axis: Axis::default(),
224			y_axis: Axis::default(),
225			style: Default::default(),
226			datasets: &[],
227		}
228	}
229}
230
231impl<'a, LX, LY> Chart<'a, LX, LY>
232where
233	LX: AsRef<str>,
234	LY: AsRef<str>,
235{
236	pub fn block(mut self, block: Block<'a>) -> Chart<'a, LX, LY> {
237		self.block = Some(block);
238		self
239	}
240
241	pub fn style(mut self, style: Style) -> Chart<'a, LX, LY> {
242		self.style = style;
243		self
244	}
245
246	pub fn x_axis(mut self, axis: Axis<'a, LX>) -> Chart<'a, LX, LY> {
247		self.x_axis = axis;
248		self
249	}
250
251	pub fn y_axis(mut self, axis: Axis<'a, LY>) -> Chart<'a, LX, LY> {
252		self.y_axis = axis;
253		self
254	}
255
256	pub fn datasets(mut self, datasets: &'a [Dataset<'a>]) -> Chart<'a, LX, LY> {
257		self.datasets = datasets;
258		self
259	}
260
261	/// Compute the internal layout of the chart given the area. If the area is too small some
262	/// elements may be automatically hidden
263	fn layout(&self, area: Rect) -> ChartLayout {
264		let mut layout = ChartLayout::default();
265		if area.height == 0 || area.width == 0 {
266			return layout;
267		}
268		let mut x = area.left();
269		let mut y = area.bottom() - 1;
270
271		if self.x_axis.labels.is_some() && y > area.top() {
272			layout.label_x = Some(y);
273			y -= 1;
274		}
275
276		if let Some(y_labels) = self.y_axis.labels {
277			let mut max_width = y_labels
278				.iter()
279				.fold(0, |acc, l| max(l.as_ref().width(), acc)) as u16;
280			if let Some(x_labels) = self.x_axis.labels {
281				if !x_labels.is_empty() {
282					max_width = max(max_width, x_labels[0].as_ref().width() as u16);
283				}
284			}
285			if x + max_width < area.right() {
286				layout.label_y = Some(x);
287				x += max_width;
288			}
289		}
290
291		if self.x_axis.labels.is_some() && y > area.top() {
292			layout.axis_x = Some(y);
293			y -= 1;
294		}
295
296		if self.y_axis.labels.is_some() && x + 1 < area.right() {
297			layout.axis_y = Some(x);
298			x += 1;
299		}
300
301		if x < area.right() && y > 1 {
302			layout.graph_area = Rect::new(x, area.top(), area.right() - x, y - area.top() + 1);
303		}
304
305		if let Some(title) = self.x_axis.title {
306			let w = title.width() as u16;
307			if w < layout.graph_area.width && layout.graph_area.height > 2 {
308				layout.title_x = Some((x + layout.graph_area.width - w, y));
309			}
310		}
311
312		if let Some(title) = self.y_axis.title {
313			let w = title.width() as u16;
314			if w + 1 < layout.graph_area.width && layout.graph_area.height > 2 {
315				layout.title_y = Some((x + 1, area.top()));
316			}
317		}
318
319		if let Some(inner_width) = self.datasets.iter().map(|d| d.name.width() as u16).max() {
320			let legend_width = inner_width + 2;
321			let legend_height = self.datasets.len() as u16 + 2;
322			if legend_width < layout.graph_area.width
323				&& legend_height < layout.graph_area.height
324				&& inner_width > 0
325			{
326				layout.legend_area = Some(Rect::new(
327					layout.graph_area.right() - legend_width,
328					layout.graph_area.top(),
329					legend_width,
330					legend_height,
331				));
332			}
333		}
334		layout
335	}
336}
337
338impl<'a, LX, LY> Widget for Chart<'a, LX, LY>
339where
340	LX: AsRef<str>,
341	LY: AsRef<str>,
342{
343	fn draw(&mut self, area: Rect, buf: &mut Buffer) {
344		let chart_area = match self.block {
345			Some(ref mut b) => {
346				b.draw(area, buf);
347				b.inner(area)
348			}
349			None => area,
350		};
351
352		let layout = self.layout(chart_area);
353		let graph_area = layout.graph_area;
354		if graph_area.width < 1 || graph_area.height < 1 {
355			return;
356		}
357
358		self.background(chart_area, buf, self.style.bg);
359
360		if let Some((x, y)) = layout.title_x {
361			let title = self.x_axis.title.unwrap();
362			buf.set_string(x, y, title, self.x_axis.style);
363		}
364
365		if let Some((x, y)) = layout.title_y {
366			let title = self.y_axis.title.unwrap();
367			buf.set_string(x, y, title, self.y_axis.style);
368		}
369
370		if let Some(y) = layout.label_x {
371			let labels = self.x_axis.labels.unwrap();
372			let total_width = labels.iter().fold(0, |acc, l| l.as_ref().width() + acc) as u16;
373			let labels_len = labels.len() as u16;
374			if total_width < graph_area.width && labels_len > 1 {
375				for (i, label) in labels.iter().enumerate() {
376					buf.set_string(
377						graph_area.left() + i as u16 * (graph_area.width - 1) / (labels_len - 1)
378							- label.as_ref().width() as u16,
379						y,
380						label.as_ref(),
381						self.x_axis.labels_style,
382					);
383				}
384			}
385		}
386
387		if let Some(x) = layout.label_y {
388			let labels = self.y_axis.labels.unwrap();
389			let labels_len = labels.len() as u16;
390			for (i, label) in labels.iter().enumerate() {
391				let dy = i as u16 * (graph_area.height - 1) / (labels_len - 1);
392				if dy < graph_area.bottom() {
393					buf.set_string(
394						x,
395						graph_area.bottom() - 1 - dy,
396						label.as_ref(),
397						self.y_axis.labels_style,
398					);
399				}
400			}
401		}
402
403		if let Some(y) = layout.axis_x {
404			for x in graph_area.left()..graph_area.right() {
405				buf.get_mut(x, y)
406					.set_symbol(symbols::line::HORIZONTAL)
407					.set_style(self.x_axis.style);
408			}
409		}
410
411		if let Some(x) = layout.axis_y {
412			for y in graph_area.top()..graph_area.bottom() {
413				buf.get_mut(x, y)
414					.set_symbol(symbols::line::VERTICAL)
415					.set_style(self.y_axis.style);
416			}
417		}
418
419		if let Some(y) = layout.axis_x {
420			if let Some(x) = layout.axis_y {
421				buf.get_mut(x, y)
422					.set_symbol(symbols::line::BOTTOM_LEFT)
423					.set_style(self.x_axis.style);
424			}
425		}
426
427		for dataset in self.datasets {
428			match dataset.marker {
429				Marker::Dot => {
430					for &(x, y) in dataset.data.iter().filter(|&&(x, y)| {
431						!(x < self.x_axis.bounds[0]
432							|| x > self.x_axis.bounds[1] || y < self.y_axis.bounds[0]
433							|| y > self.y_axis.bounds[1])
434					}) {
435						let dy = ((self.y_axis.bounds[1] - y) * f64::from(graph_area.height - 1)
436							/ (self.y_axis.bounds[1] - self.y_axis.bounds[0])) as u16;
437						let dx = ((x - self.x_axis.bounds[0]) * f64::from(graph_area.width - 1)
438							/ (self.x_axis.bounds[1] - self.x_axis.bounds[0])) as u16;
439
440						buf.get_mut(graph_area.left() + dx, graph_area.top() + dy)
441							.set_symbol(symbols::DOT)
442							.set_fg(dataset.style.fg)
443							.set_bg(dataset.style.bg);
444					}
445				}
446				Marker::Braille => {
447					Canvas::default()
448						.background_color(self.style.bg)
449						.x_bounds(self.x_axis.bounds)
450						.y_bounds(self.y_axis.bounds)
451						.paint(|ctx| {
452							ctx.draw(&Points {
453								coords: dataset.data,
454								color: dataset.style.fg,
455							});
456						})
457						.draw(graph_area, buf);
458				}
459			}
460		}
461
462		if let Some(legend_area) = layout.legend_area {
463			Block::default()
464				.borders(Borders::ALL)
465				.draw(legend_area, buf);
466			for (i, dataset) in self.datasets.iter().enumerate() {
467				buf.set_string(
468					legend_area.x + 1,
469					legend_area.y + 1 + i as u16,
470					dataset.name,
471					dataset.style,
472				);
473			}
474		}
475	}
476}