1use std::borrow::Cow;
6
7use polars::prelude::Schema;
8
9use crate::chart::chart_data::{
10 self, BarData, BoxPlotData, ChartXRangeResult, HeatmapData, HistogramData, KdeData, RowsRead,
11 XAxisTemporalKind,
12};
13use crate::chart::chart_modal::{Aggregate, ChartModal, ChartSpec, Mark};
14use crate::numfmt;
15use crate::widgets::axis_numbers::AxisNumbers;
16
17#[derive(Debug, Clone)]
20pub enum PlotData {
21 Lines(LinesData),
22 XRange(ChartXRangeResult),
24 Histogram(HistogramData),
25 Box(BoxPlotData),
26 Kde(KdeData),
27 Heatmap(HeatmapData),
28 Bars(BarData),
29}
30
31#[derive(Debug, Clone, Default)]
33pub struct LinesData {
34 pub names: Vec<String>,
36 pub series: Vec<Vec<(f64, f64)>>,
37 pub series_log: Option<Vec<Vec<(f64, f64)>>>,
40 pub breaks: Vec<Vec<usize>>,
42 pub xs: Vec<f64>,
44 pub bounds: Option<[f64; 4]>,
46 pub x_axis_kind: XAxisTemporalKind,
47 pub rows: RowsRead,
48 pub rows_note: Option<String>,
50 pub other: bool,
52}
53
54#[derive(Debug, Clone, Copy)]
56pub struct Drawn<'a> {
57 pub index: usize,
60 pub name: &'a str,
61 pub points: &'a [(f64, f64)],
62 pub breaks: &'a [usize],
63 pub other: bool,
64}
65
66impl LinesData {
67 pub fn new(grouped: chart_data::GroupedSeries, rows_note: Option<String>) -> Self {
70 Self {
71 series_log: None,
72 xs: crate::widgets::crosshair::xs(&grouped.series),
73 bounds: extent(&grouped.series),
74 names: grouped.names,
75 series: grouped.series,
76 breaks: grouped.breaks,
77 x_axis_kind: grouped.x_axis_kind,
78 rows: grouped.rows,
79 rows_note,
80 other: grouped.other,
81 }
82 }
83
84 pub fn keep_log(&mut self, log: bool) {
86 match (log, self.series_log.is_some()) {
87 (true, false) => self.series_log = Some(log_series(&self.series)),
88 (false, true) => self.series_log = None,
89 _ => {}
90 }
91 }
92
93 pub fn shown(&self, log: bool) -> &[Vec<(f64, f64)>] {
95 match (&self.series_log, log) {
96 (Some(logged), true) => logged,
97 _ => &self.series,
98 }
99 }
100
101 pub fn shown_bounds(&self, log: bool) -> Option<[f64; 4]> {
104 match self.bounds {
105 Some([x0, x1, y0, y1]) if log && self.series_log.is_some() => {
106 Some([x0, x1, log_y(y0), log_y(y1)])
107 }
108 Some(bounds) => Some(bounds),
109 None => extent(self.shown(log)),
110 }
111 }
112
113 pub fn drawn(&self, log: bool) -> impl Iterator<Item = Drawn<'_>> {
115 let last = self.names.len().saturating_sub(1);
116 self.shown(log)
117 .iter()
118 .zip(self.names.iter())
119 .enumerate()
120 .filter(|(_, (points, _))| !points.is_empty())
121 .map(move |(index, (points, name))| Drawn {
122 index,
123 name,
124 points,
125 breaks: self.breaks.get(index).map_or(&[][..], Vec::as_slice),
126 other: self.other && index == last,
127 })
128 }
129}
130
131pub fn extent(series: &[Vec<(f64, f64)>]) -> Option<[f64; 4]> {
133 let mut points = series.iter().flatten().peekable();
134 points.peek()?;
135 Some(points.fold(
136 [
137 f64::INFINITY,
138 f64::NEG_INFINITY,
139 f64::INFINITY,
140 f64::NEG_INFINITY,
141 ],
142 |[x0, x1, y0, y1], &(x, y)| [x0.min(x), x1.max(x), y0.min(y), y1.max(y)],
143 ))
144}
145
146fn log_y(y: f64) -> f64 {
148 y.max(0.0).ln_1p()
149}
150
151fn log_series(series: &[Vec<(f64, f64)>]) -> Vec<Vec<(f64, f64)>> {
152 series
153 .iter()
154 .map(|points| points.iter().map(|&(x, y)| (x, log_y(y))).collect())
155 .collect()
156}
157
158impl PlotData {
159 pub fn is_empty(&self) -> bool {
161 match self {
162 Self::Lines(lines) => lines.drawn(false).next().is_none(),
163 Self::XRange(_) => true,
164 Self::Bars(data) => data.bars.is_empty(),
165 Self::Histogram(data) => data.bins.is_empty(),
166 Self::Kde(data) => data.series.is_empty(),
167 Self::Box(data) => data.stats.is_empty(),
168 Self::Heatmap(data) => data.counts.is_empty() || data.max_count <= 0.0,
169 }
170 }
171
172 pub fn notes(&self, middot: &str) -> Vec<String> {
175 let rows_of = |rows: usize| crate::home::discover::format_rows(rows);
176 match self {
177 Self::Bars(d) => {
178 let mut notes = chart_data::chart_notes(&d.rows, None, middot);
179 if let Some(note) = &d.rows_note {
180 notes.push(note.clone());
181 } else if let Some(rows) = d.counted {
182 notes.push(format!("counts of {} rows", rows_of(rows)));
183 } else if d.rows.sample_size.is_none() && !d.value_column.is_empty() {
184 notes.push(format!(
185 "all {} rows",
186 numfmt::group_chrome(d.rows.total_rows)
187 ));
188 }
189 if d.no_value > 0 {
190 let noun = if d.no_value == 1 {
191 "category"
192 } else {
193 "categories"
194 };
195 notes.push(format!(
196 "{} {noun} without a value",
197 numfmt::group_chrome(d.no_value)
198 ));
199 }
200 notes
201 }
202 Self::Lines(c) if c.rows_note.is_some() => c.rows_note.iter().cloned().collect(),
203 Self::Lines(c) => chart_data::chart_notes(&c.rows, None, middot),
204 Self::XRange(c) => chart_data::chart_notes(&c.rows, None, middot),
205 Self::Histogram(d) => chart_data::chart_notes(&d.rows, d.clipped.as_ref(), middot),
206 Self::Box(d) => {
207 let mut notes = chart_data::chart_notes(&d.rows, d.clipped.as_ref(), middot);
208 if d.of > 0 {
209 notes.push(format!(
210 "the {} largest of {} categories",
211 d.stats.len(),
212 numfmt::group_chrome(d.of)
213 ));
214 }
215 notes
216 }
217 Self::Kde(d) => chart_data::chart_notes(&d.rows, d.clipped.as_ref(), middot),
218 Self::Heatmap(d) => chart_data::chart_notes(&d.rows, None, middot),
219 }
220 }
221}
222
223#[derive(Debug, Clone, Default)]
225pub struct Axis {
226 pub title: String,
227 pub numbers: AxisNumbers,
228 pub kind: XAxisTemporalKind,
229 pub log: bool,
231}
232
233#[derive(Debug, Clone)]
237pub struct Plot<'a> {
238 pub data: Cow<'a, PlotData>,
239 pub x: Axis,
240 pub y: Axis,
241 pub scatter: bool,
242 pub y_from_zero: bool,
243}
244
245impl Plot<'_> {
246 pub fn into_owned(self) -> Plot<'static> {
248 Plot {
249 data: Cow::Owned(self.data.into_owned()),
250 ..self
251 }
252 }
253
254 pub fn drawn(&self) -> impl Iterator<Item = Drawn<'_>> {
256 let lines = match &*self.data {
257 PlotData::Lines(lines) => Some(lines),
258 _ => None,
259 };
260 lines.into_iter().flat_map(|lines| lines.drawn(self.y.log))
261 }
262 pub fn curves(&self) -> Vec<Curve<'_>> {
266 let curves: Vec<Curve<'_>> = match &*self.data {
267 PlotData::Lines(_) => self
268 .drawn()
269 .enumerate()
270 .map(|(slot, s)| Curve {
271 index: s.index,
272 slot,
273 points: Cow::Borrowed(s.points),
274 breaks: s.breaks,
275 other: s.other,
276 })
277 .collect(),
278 PlotData::Kde(data) => data
279 .series
280 .iter()
281 .enumerate()
282 .map(|(i, s)| Curve {
283 index: i,
284 slot: i,
285 points: Cow::Borrowed(&s.points),
286 breaks: &[],
287 other: data.other && i + 1 == data.series.len(),
288 })
289 .collect(),
290 PlotData::Histogram(data) if !data.groups.is_empty() => data
291 .step_outlines()
292 .into_iter()
293 .enumerate()
294 .map(|(i, points)| Curve {
295 index: i,
296 slot: i,
297 points: Cow::Owned(points),
298 breaks: &[],
299 other: data.other && i + 1 == data.groups.len(),
300 })
301 .collect(),
302 _ => Vec::new(),
303 };
304 let other = curves.iter().position(|c| c.other);
305 let mut curves: Vec<Option<Curve<'_>>> = curves.into_iter().map(Some).collect();
306 chart_data::drawing_order(curves.len(), other)
307 .filter_map(|i| curves[i].take())
308 .collect()
309 }
310
311 pub fn legend(&self) -> Vec<&str> {
313 match &*self.data {
314 PlotData::Lines(_) => self.drawn().map(|s| s.name).collect(),
315 PlotData::Bars(data) => data.groups.iter().map(String::as_str).collect(),
316 PlotData::Histogram(data) => data.groups.iter().map(|g| g.name.as_str()).collect(),
317 PlotData::Kde(data) => data.series.iter().map(|s| s.name.as_str()).collect(),
318 PlotData::Box(_) | PlotData::Heatmap(_) | PlotData::XRange(_) => Vec::new(),
319 }
320 }
321
322 pub fn other_at(&self) -> Option<usize> {
324 match &*self.data {
325 PlotData::Lines(_) => self.drawn().position(|s| s.other),
326 PlotData::Bars(data) => chart_data::other_at(data.other, data.groups.len()),
327 _ => self.curves().iter().find(|c| c.other).map(|c| c.slot),
328 }
329 }
330}
331
332#[derive(Debug, Clone)]
334pub struct Curve<'a> {
335 pub index: usize,
338 pub slot: usize,
340 pub points: Cow<'a, [(f64, f64)]>,
341 pub breaks: &'a [usize],
343 pub other: bool,
345}
346
347pub struct PlotContext<'a> {
350 pub modal: &'a ChartModal,
351 pub spec: &'a ChartSpec,
353 pub numbers: &'a numfmt::NumberFormatSettings,
354 pub schema: Option<&'a Schema>,
355}
356
357pub fn plot<'a>(data: Option<&'a PlotData>, context: &PlotContext<'_>) -> Option<Plot<'a>> {
360 let PlotContext {
361 modal,
362 spec,
363 numbers,
364 schema,
365 } = *context;
366 let column = |name: &str| AxisNumbers::column(numbers, schema, name);
367 let title = |name: &str| modal.axis_title(name);
368 let axis = |title: String, numbers: AxisNumbers| Axis {
369 title,
370 numbers,
371 ..Default::default()
372 };
373 let encoding = &spec.encoding;
374 let x_name = encoding.x.field.as_deref();
375 let ys = &encoding.y.field;
376 let (x, y) = match (spec.mark, data) {
377 (Mark::Line | Mark::Scatter, data) => return Some(lines(data, context)),
378 (Mark::Histogram, Some(PlotData::Histogram(data))) => (
379 axis(title(&data.column), column(&data.column)),
380 if data.share {
381 axis("Share".to_string(), AxisNumbers::measure(numbers, "Share"))
382 } else {
383 axis("Count".to_string(), AxisNumbers::count(numbers))
384 },
385 ),
386 (Mark::Kde, Some(PlotData::Kde(_))) => (
387 axis(
388 x_name.map(title).unwrap_or_default(),
389 x_name.map(column).unwrap_or_default().fractional(),
390 ),
391 axis(
392 "Density".to_string(),
393 AxisNumbers::measure(numbers, "Density"),
394 ),
395 ),
396 (Mark::Box, Some(PlotData::Box(_))) => (
397 axis(
398 x_name.unwrap_or_default().to_string(),
399 AxisNumbers::default(),
400 ),
401 axis(
402 ys.first().map(|y| title(y)).unwrap_or_default(),
403 AxisNumbers::columns(numbers, schema, ys),
404 ),
405 ),
406 (Mark::Heatmap, Some(PlotData::Heatmap(data))) => (
407 axis(title(&data.x_column), column(&data.x_column)),
408 axis(title(&data.y_column), column(&data.y_column)),
409 ),
410 (Mark::Bar, Some(PlotData::Bars(data))) => (
411 axis(
412 data.value_column.clone(),
413 AxisNumbers {
414 format: data.value_format(numbers),
415 whole: data.value_dtype.is_integer(),
416 },
417 ),
418 Axis::default(),
419 ),
420 _ => return None,
421 };
422 Some(Plot {
423 data: Cow::Borrowed(data?),
424 x,
425 y,
426 scatter: false,
427 y_from_zero: false,
428 })
429}
430
431fn lines<'a>(data: Option<&'a PlotData>, context: &PlotContext<'_>) -> Plot<'a> {
434 let PlotContext {
435 modal,
436 spec,
437 numbers,
438 schema,
439 } = *context;
440 let encoding = &spec.encoding;
441 let ys = &encoding.y.field;
442 let aggregate = encoding.y.aggregate;
443 let y_numbers = match aggregate {
444 Aggregate::Count | Aggregate::Distinct => AxisNumbers::count(numbers),
445 a if a.is_fractional() => AxisNumbers::columns(numbers, schema, ys).fractional(),
447 _ => AxisNumbers::columns(numbers, schema, ys),
448 };
449 let y_title = if aggregate == Aggregate::Count {
450 "count".to_string()
451 } else {
452 ys.iter()
453 .map(|y| modal.axis_title(y))
454 .collect::<Vec<_>>()
455 .join(", ")
456 };
457 let x_name = encoding.x.field.as_deref();
458 let kind = match data {
459 Some(PlotData::Lines(lines)) => lines.x_axis_kind,
460 Some(PlotData::XRange(range)) => range.x_axis_kind,
461 _ => match (x_name, schema) {
463 (Some(x), Some(schema)) => chart_data::x_axis_temporal_kind_for_column(schema, x),
464 _ => XAxisTemporalKind::Numeric,
465 },
466 };
467 let data = match data {
468 Some(PlotData::Lines(lines)) if modal.log_scale && lines.series_log.is_none() => {
471 let mut lines = lines.clone();
472 lines.keep_log(true);
473 Cow::Owned(PlotData::Lines(lines))
474 }
475 Some(data @ (PlotData::Lines(_) | PlotData::XRange(_))) => Cow::Borrowed(data),
476 _ => Cow::Owned(PlotData::Lines(LinesData::default())),
477 };
478 Plot {
479 data,
480 x: Axis {
481 title: x_name.map(|x| modal.axis_title(x)).unwrap_or_default(),
482 numbers: x_name
483 .map(|x| AxisNumbers::column(numbers, schema, x))
484 .unwrap_or_default(),
485 kind,
486 log: false,
487 },
488 y: Axis {
489 title: y_title,
490 numbers: y_numbers,
491 log: modal.log_scale,
492 ..Default::default()
493 },
494 scatter: spec.mark == Mark::Scatter,
495 y_from_zero: modal.y_starts_at_zero,
496 }
497}