Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
pola-rs
GitHub Repository: pola-rs/polars
Path: blob/main/crates/polars-plan/src/plans/ir/tree_format.rs
8431 views
1
use std::fmt::{self, Write};
2
3
use polars_core::error::*;
4
use polars_utils::format_list_truncated;
5
6
use crate::constants;
7
use crate::plans::ir::IRPlanRef;
8
use crate::plans::visitor::{VisitRecursion, Visitor};
9
use crate::prelude::ir::format::ColumnsDisplay;
10
use crate::prelude::visitor::AexprNode;
11
use crate::prelude::*;
12
13
pub struct TreeFmtNode<'a> {
14
h: Option<String>,
15
content: TreeFmtNodeContent<'a>,
16
17
lp: IRPlanRef<'a>,
18
}
19
20
pub struct TreeFmtAExpr<'a>(&'a AExpr);
21
22
/// Hack UpperExpr trait to get a kind of formatting that doesn't traverse the nodes.
23
/// So we can format with {foo:E}
24
impl fmt::Display for TreeFmtAExpr<'_> {
25
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26
let s = match self.0 {
27
AExpr::Element => "element()",
28
AExpr::Explode { expr: _, options } => {
29
f.write_str("explode(")?;
30
match (options.empty_as_null, options.keep_nulls) {
31
(true, true) => {},
32
(true, false) => f.write_str("keep_nulls=false")?,
33
(false, true) => f.write_str("empty_as_null=false")?,
34
(false, false) => f.write_str("empty_as_null=false, keep_nulls=false")?,
35
}
36
return f.write_char(')');
37
},
38
AExpr::Column(name) => return write!(f, "col({name})"),
39
#[cfg(feature = "dtype-struct")]
40
AExpr::StructField(name) => return write!(f, "field({name})"),
41
AExpr::Literal(lv) => return write!(f, "lit({lv:?})"),
42
AExpr::BinaryExpr { op, .. } => return write!(f, "binary: {op}"),
43
AExpr::Cast { dtype, options, .. } => {
44
return if options.is_strict() {
45
write!(f, "strict cast({dtype})")
46
} else {
47
write!(f, "cast({dtype})")
48
};
49
},
50
AExpr::Sort { options, .. } => {
51
return write!(
52
f,
53
"sort: {}{}{}",
54
options.descending as u8, options.nulls_last as u8, options.multithreaded as u8
55
);
56
},
57
AExpr::Gather { .. } => "gather",
58
AExpr::SortBy { sort_options, .. } => {
59
write!(f, "sort_by:")?;
60
for i in &sort_options.descending {
61
write!(f, "{}", *i as u8)?;
62
}
63
for i in &sort_options.nulls_last {
64
write!(f, "{}", *i as u8)?;
65
}
66
write!(f, "{}", sort_options.multithreaded as u8)?;
67
return Ok(());
68
},
69
AExpr::Filter { .. } => "filter",
70
AExpr::Agg(a) => {
71
let s: &str = a.into();
72
return write!(f, "{}", s.to_lowercase());
73
},
74
AExpr::Ternary { .. } => "ternary",
75
AExpr::AnonymousFunction { fmt_str, .. } => {
76
return write!(f, "anonymous_function: {fmt_str}");
77
},
78
AExpr::AnonymousAgg { fmt_str, .. } => {
79
return write!(f, "anonymous_agg: {fmt_str}");
80
},
81
AExpr::Eval { .. } => "list.eval",
82
#[cfg(feature = "dtype-struct")]
83
AExpr::StructEval { .. } => "struct.with_fields",
84
AExpr::Function { function, .. } => return write!(f, "function: {function}"),
85
#[cfg(feature = "dynamic_group_by")]
86
AExpr::Rolling { .. } => "rolling",
87
AExpr::Over { .. } => "window",
88
AExpr::Slice { .. } => "slice",
89
AExpr::Len => constants::LEN,
90
};
91
92
write!(f, "{s}")
93
}
94
}
95
96
pub enum TreeFmtNodeContent<'a> {
97
Expression(&'a ExprIR),
98
LogicalPlan(Node),
99
}
100
101
struct TreeFmtNodeData<'a>(String, Vec<TreeFmtNode<'a>>);
102
103
fn with_header(header: &Option<String>, text: &str) -> String {
104
if let Some(header) = header {
105
format!("{header}\n{text}")
106
} else {
107
text.to_string()
108
}
109
}
110
111
#[cfg(feature = "regex")]
112
fn multiline_expression(expr: &str) -> std::borrow::Cow<'_, str> {
113
polars_utils::regex_cache::cached_regex! {
114
static RE = r"([\)\]])(\.[a-z0-9]+\()";
115
}
116
RE.replace_all(expr, "$1\n $2")
117
}
118
119
impl<'a> TreeFmtNode<'a> {
120
pub fn root_logical_plan(lp: IRPlanRef<'a>) -> Self {
121
Self {
122
h: None,
123
content: TreeFmtNodeContent::LogicalPlan(lp.lp_top),
124
125
lp,
126
}
127
}
128
129
pub fn lp_node(&self, h: Option<String>, root: Node) -> Self {
130
Self {
131
h,
132
content: TreeFmtNodeContent::LogicalPlan(root),
133
134
lp: self.lp,
135
}
136
}
137
138
pub fn expr_node(&self, h: Option<String>, expr: &'a ExprIR) -> Self {
139
Self {
140
h,
141
content: TreeFmtNodeContent::Expression(expr),
142
143
lp: self.lp,
144
}
145
}
146
147
pub fn traverse(&self, visitor: &mut TreeFmtVisitor) {
148
let TreeFmtNodeData(title, child_nodes) = self.node_data();
149
150
if visitor.levels.len() <= visitor.depth {
151
visitor.levels.push(vec![]);
152
}
153
154
let row = visitor.levels.get_mut(visitor.depth).unwrap();
155
row.resize(visitor.width + 1, "".to_string());
156
157
row[visitor.width] = title;
158
visitor.prev_depth = visitor.depth;
159
visitor.depth += 1;
160
161
for child in &child_nodes {
162
child.traverse(visitor);
163
}
164
165
visitor.depth -= 1;
166
visitor.width += if visitor.prev_depth == visitor.depth {
167
1
168
} else {
169
0
170
};
171
}
172
173
fn node_data(&self) -> TreeFmtNodeData<'_> {
174
use {TreeFmtNodeContent as C, TreeFmtNodeData as ND, with_header as wh};
175
176
let lp = &self.lp;
177
let h = &self.h;
178
179
use IR::*;
180
match self.content {
181
#[cfg(feature = "regex")]
182
C::Expression(expr) => ND(
183
wh(
184
h,
185
&multiline_expression(&expr.display(self.lp.expr_arena).to_string()),
186
),
187
vec![],
188
),
189
#[cfg(not(feature = "regex"))]
190
C::Expression(expr) => ND(wh(h, &expr.display(self.lp.expr_arena).to_string()), vec![]),
191
C::LogicalPlan(lp_top) => {
192
match self.lp.with_root(lp_top).root() {
193
#[cfg(feature = "python")]
194
PythonScan { .. } => ND(wh(h, &lp.describe()), vec![]),
195
Scan { .. } => ND(wh(h, &lp.describe()), vec![]),
196
DataFrameScan {
197
schema,
198
output_schema,
199
..
200
} => {
201
let (n_columns, projected) = if let Some(schema) = output_schema {
202
(
203
format!("{}", schema.len()),
204
format!(
205
": {};",
206
format_list_truncated!(schema.iter_names(), 4, '"')
207
),
208
)
209
} else {
210
("*".to_string(), "".to_string())
211
};
212
ND(
213
wh(
214
h,
215
&format!(
216
"DF {}\nPROJECT{} {}/{} COLUMNS",
217
format_list_truncated!(schema.iter_names(), 4, '"'),
218
projected,
219
n_columns,
220
schema.len()
221
),
222
),
223
vec![],
224
)
225
},
226
227
Union { inputs, .. } => ND(
228
wh(
229
h,
230
// THis is commented out, but must be restored when we convert to IR's.
231
// &(if let Some(slice) = options.slice {
232
// format!("SLICED UNION: {slice:?}")
233
// } else {
234
// "UNION".to_string()
235
// }),
236
"UNION",
237
),
238
inputs
239
.iter()
240
.enumerate()
241
.map(|(i, lp_root)| self.lp_node(Some(format!("PLAN {i}:")), *lp_root))
242
.collect(),
243
),
244
HConcat { inputs, .. } => ND(
245
wh(h, "HCONCAT"),
246
inputs
247
.iter()
248
.enumerate()
249
.map(|(i, lp_root)| self.lp_node(Some(format!("PLAN {i}:")), *lp_root))
250
.collect(),
251
),
252
Cache { input, id } => ND(
253
wh(h, &format!("CACHE[id: {id}]")),
254
vec![self.lp_node(None, *input)],
255
),
256
Filter { input, predicate } => ND(
257
wh(h, "FILTER"),
258
vec![
259
self.expr_node(Some("predicate:".to_string()), predicate),
260
self.lp_node(Some("FROM:".to_string()), *input),
261
],
262
),
263
Select { expr, input, .. } => ND(
264
wh(h, "SELECT"),
265
expr.iter()
266
.map(|expr| self.expr_node(Some("expression:".to_string()), expr))
267
.chain([self.lp_node(Some("FROM:".to_string()), *input)])
268
.collect(),
269
),
270
Sort {
271
input, by_column, ..
272
} => ND(
273
wh(h, "SORT BY"),
274
by_column
275
.iter()
276
.map(|expr| self.expr_node(Some("expression:".to_string()), expr))
277
.chain([self.lp_node(None, *input)])
278
.collect(),
279
),
280
GroupBy {
281
input,
282
keys,
283
aggs,
284
maintain_order,
285
..
286
} => ND(
287
wh(
288
h,
289
&format!("AGGREGATE[maintain_order: {}]", *maintain_order),
290
),
291
aggs.iter()
292
.map(|expr| self.expr_node(Some("expression:".to_string()), expr))
293
.chain(keys.iter().map(|expr| {
294
self.expr_node(Some("aggregate by:".to_string()), expr)
295
}))
296
.chain([self.lp_node(Some("FROM:".to_string()), *input)])
297
.collect(),
298
),
299
Join {
300
input_left,
301
input_right,
302
left_on,
303
right_on,
304
options,
305
..
306
} => ND(
307
wh(h, &format!("{} JOIN", options.args.how)),
308
left_on
309
.iter()
310
.map(|expr| self.expr_node(Some("left on:".to_string()), expr))
311
.chain([self.lp_node(Some("LEFT PLAN:".to_string()), *input_left)])
312
.chain(
313
right_on.iter().map(|expr| {
314
self.expr_node(Some("right on:".to_string()), expr)
315
}),
316
)
317
.chain([self.lp_node(Some("RIGHT PLAN:".to_string()), *input_right)])
318
.collect(),
319
),
320
HStack { input, exprs, .. } => ND(
321
wh(h, "WITH_COLUMNS"),
322
exprs
323
.iter()
324
.map(|expr| self.expr_node(Some("expression:".to_string()), expr))
325
.chain([self.lp_node(None, *input)])
326
.collect(),
327
),
328
Distinct { input, options } => ND(
329
wh(
330
h,
331
&format!(
332
"UNIQUE[maintain_order: {:?}, keep_strategy: {:?}] BY {:?}",
333
options.maintain_order, options.keep_strategy, options.subset
334
),
335
),
336
vec![self.lp_node(None, *input)],
337
),
338
Slice { input, offset, len } => ND(
339
wh(h, &format!("SLICE[offset: {offset}, len: {len}]")),
340
vec![self.lp_node(None, *input)],
341
),
342
MapFunction { input, function } => ND(
343
wh(h, &format!("{function}")),
344
vec![self.lp_node(None, *input)],
345
),
346
ExtContext { input, .. } => {
347
ND(wh(h, "EXTERNAL_CONTEXT"), vec![self.lp_node(None, *input)])
348
},
349
Sink { input, payload } => ND(
350
wh(
351
h,
352
match payload {
353
SinkTypeIR::Memory => "SINK (memory)",
354
SinkTypeIR::Callback(..) => "SINK (callback)",
355
SinkTypeIR::File { .. } => "SINK (file)",
356
SinkTypeIR::Partitioned { .. } => "SINK (partition)",
357
},
358
),
359
vec![self.lp_node(None, *input)],
360
),
361
SinkMultiple { inputs } => ND(
362
wh(h, "SINK_MULTIPLE"),
363
inputs
364
.iter()
365
.enumerate()
366
.map(|(i, lp_root)| self.lp_node(Some(format!("PLAN {i}:")), *lp_root))
367
.collect(),
368
),
369
SimpleProjection { input, columns } => {
370
let num_columns = columns.as_ref().len();
371
let total_columns = lp.lp_arena.get(*input).schema(lp.lp_arena).len();
372
373
let columns = ColumnsDisplay(columns.as_ref());
374
ND(
375
wh(
376
h,
377
&format!("simple π {num_columns}/{total_columns} [{columns}]"),
378
),
379
vec![self.lp_node(None, *input)],
380
)
381
},
382
#[cfg(feature = "merge_sorted")]
383
MergeSorted {
384
input_left,
385
input_right,
386
key,
387
} => ND(
388
wh(h, &format!("MERGE SORTED ON '{key}")),
389
[self.lp_node(Some("LEFT PLAN:".to_string()), *input_left)]
390
.into_iter()
391
.chain([self.lp_node(Some("RIGHT PLAN:".to_string()), *input_right)])
392
.collect(),
393
),
394
Invalid => ND(wh(h, "INVALID"), vec![]),
395
}
396
},
397
}
398
}
399
}
400
401
#[derive(Default)]
402
pub enum TreeFmtVisitorDisplay {
403
#[default]
404
DisplayText,
405
DisplayDot,
406
}
407
408
#[derive(Default)]
409
pub(crate) struct TreeFmtVisitor {
410
levels: Vec<Vec<String>>,
411
prev_depth: usize,
412
depth: usize,
413
width: usize,
414
pub(crate) display: TreeFmtVisitorDisplay,
415
}
416
417
impl Visitor for TreeFmtVisitor {
418
type Node = AexprNode;
419
type Arena = Arena<AExpr>;
420
421
/// Invoked before any children of `node` are visited.
422
fn pre_visit(
423
&mut self,
424
node: &Self::Node,
425
arena: &Self::Arena,
426
) -> PolarsResult<VisitRecursion> {
427
let repr = TreeFmtAExpr(arena.get(node.node()));
428
let repr = repr.to_string();
429
430
if self.levels.len() <= self.depth {
431
self.levels.push(vec![])
432
}
433
434
// the post-visit ensures the width of this node is known
435
let row = self.levels.get_mut(self.depth).unwrap();
436
437
// set default values to ensure we format at the right width
438
row.resize(self.width + 1, "".to_string());
439
row[self.width] = repr;
440
441
// before entering a depth-first branch we preserve the depth to control the width increase
442
// in the post-visit
443
self.prev_depth = self.depth;
444
445
// we will enter depth first, we enter child so depth increases
446
self.depth += 1;
447
448
Ok(VisitRecursion::Continue)
449
}
450
451
fn post_visit(
452
&mut self,
453
_node: &Self::Node,
454
_arena: &Self::Arena,
455
) -> PolarsResult<VisitRecursion> {
456
// we finished this branch so we decrease in depth, back the caller node
457
self.depth -= 1;
458
459
// because we traverse depth first
460
// the width is increased once after one or more depth-first branches
461
// this way we avoid empty columns in the resulting tree representation
462
self.width += if self.prev_depth == self.depth { 1 } else { 0 };
463
464
Ok(VisitRecursion::Continue)
465
}
466
}
467
468
/// Calculates the number of digits in a `usize` number
469
/// Useful for the alignment of `usize` values when they are displayed
470
fn digits(n: usize) -> usize {
471
if n == 0 {
472
1
473
} else {
474
f64::log10(n as f64) as usize + 1
475
}
476
}
477
478
/// Meta-info of a column in a populated `TreeFmtVisitor` required for the pretty-print of a tree
479
#[derive(Clone, Default, Debug)]
480
struct TreeViewColumn {
481
offset: usize,
482
width: usize,
483
center: usize,
484
}
485
486
/// Meta-info of a column in a populated `TreeFmtVisitor` required for the pretty-print of a tree
487
#[derive(Clone, Default, Debug)]
488
struct TreeViewRow {
489
offset: usize,
490
height: usize,
491
center: usize,
492
}
493
494
/// Meta-info of a cell in a populated `TreeFmtVisitor`
495
#[derive(Clone, Default, Debug)]
496
struct TreeViewCell<'a> {
497
text: Vec<&'a str>,
498
/// A `Vec` of indices of `TreeViewColumn`-s stored elsewhere in another `Vec`
499
/// For a cell on a row `i` these indices point to the columns that contain child-cells on a
500
/// row `i + 1` (if the latter exists)
501
/// NOTE: might warrant a rethink should this code become used broader
502
children_columns: Vec<usize>,
503
}
504
505
/// The complete intermediate representation of a `TreeFmtVisitor` that can be drawn on a `Canvas`
506
/// down the line
507
#[derive(Default, Debug)]
508
struct TreeView<'a> {
509
n_rows: usize,
510
n_rows_width: usize,
511
matrix: Vec<Vec<TreeViewCell<'a>>>,
512
/// NOTE: `TreeViewCell`'s `children_columns` field contains indices pointing at the elements
513
/// of this `Vec`
514
columns: Vec<TreeViewColumn>,
515
rows: Vec<TreeViewRow>,
516
}
517
518
// NOTE: the code below this line is full of hardcoded integer offsets which may not be a big
519
// problem as long as it remains the private implementation of the pretty-print
520
/// The conversion from a reference to `levels` field of a `TreeFmtVisitor`
521
impl<'a> From<&'a [Vec<String>]> for TreeView<'a> {
522
#[allow(clippy::needless_range_loop)]
523
fn from(value: &'a [Vec<String>]) -> Self {
524
let n_rows = value.len();
525
let n_cols = value.iter().map(|row| row.len()).max().unwrap_or(0);
526
if n_rows == 0 || n_cols == 0 {
527
return TreeView::default();
528
}
529
// the character-width of the highest index of a row
530
let n_rows_width = digits(n_rows - 1);
531
532
let mut matrix = vec![vec![TreeViewCell::default(); n_cols]; n_rows];
533
for i in 0..n_rows {
534
for j in 0..n_cols {
535
if j < value[i].len() && !value[i][j].is_empty() {
536
matrix[i][j].text = value[i][j].split('\n').collect();
537
if i < n_rows - 1 {
538
if j < value[i + 1].len() && !value[i + 1][j].is_empty() {
539
matrix[i][j].children_columns.push(j);
540
}
541
for k in j + 1..n_cols {
542
if (k >= value[i].len() || value[i][k].is_empty())
543
&& k < value[i + 1].len()
544
{
545
if !value[i + 1][k].is_empty() {
546
matrix[i][j].children_columns.push(k);
547
}
548
} else {
549
break;
550
}
551
}
552
}
553
}
554
}
555
}
556
557
let mut y_offset = 3;
558
let mut rows = vec![TreeViewRow::default(); n_rows];
559
for i in 0..n_rows {
560
let mut height = 0;
561
for j in 0..n_cols {
562
height = [matrix[i][j].text.len(), height].into_iter().max().unwrap();
563
}
564
height += 2;
565
rows[i].offset = y_offset;
566
rows[i].height = height;
567
rows[i].center = height / 2;
568
y_offset += height + 3;
569
}
570
571
let mut x_offset = n_rows_width + 4;
572
let mut columns = vec![TreeViewColumn::default(); n_cols];
573
// the two nested loops below are those `needless_range_loop`s
574
// more readable this way to my taste
575
for j in 0..n_cols {
576
let mut width = 0;
577
for i in 0..n_rows {
578
width = [
579
matrix[i][j].text.iter().map(|l| l.len()).max().unwrap_or(0),
580
width,
581
]
582
.into_iter()
583
.max()
584
.unwrap();
585
}
586
width += 6;
587
columns[j].offset = x_offset;
588
columns[j].width = width;
589
columns[j].center = width / 2 + width % 2;
590
x_offset += width;
591
}
592
593
Self {
594
n_rows,
595
n_rows_width,
596
matrix,
597
columns,
598
rows,
599
}
600
}
601
}
602
603
/// The basic charset that's used for drawing lines and boxes on a `Canvas`
604
struct Glyphs {
605
void: char,
606
vertical_line: char,
607
horizontal_line: char,
608
top_left_corner: char,
609
top_right_corner: char,
610
bottom_left_corner: char,
611
bottom_right_corner: char,
612
tee_down: char,
613
tee_up: char,
614
}
615
616
impl Default for Glyphs {
617
fn default() -> Self {
618
Self {
619
void: ' ',
620
vertical_line: '│',
621
horizontal_line: '─',
622
top_left_corner: '╭',
623
top_right_corner: '╮',
624
bottom_left_corner: '╰',
625
bottom_right_corner: '╯',
626
tee_down: '┬',
627
tee_up: '┴',
628
}
629
}
630
}
631
632
/// A `Point` on a `Canvas`
633
#[derive(Clone, Copy)]
634
struct Point(usize, usize);
635
636
/// The orientation of a line on a `Canvas`
637
#[derive(Clone, Copy)]
638
enum Orientation {
639
Vertical,
640
Horizontal,
641
}
642
643
/// `Canvas`
644
struct Canvas {
645
width: usize,
646
height: usize,
647
canvas: Vec<Vec<char>>,
648
glyphs: Glyphs,
649
}
650
651
impl Canvas {
652
fn new(width: usize, height: usize, glyphs: Glyphs) -> Self {
653
Self {
654
width,
655
height,
656
canvas: vec![vec![glyphs.void; width]; height],
657
glyphs,
658
}
659
}
660
661
/// Draws a single `symbol` on the `Canvas`
662
/// NOTE: The `Point`s that lay outside of the `Canvas` are quietly ignored
663
fn draw_symbol(&mut self, point: Point, symbol: char) {
664
let Point(x, y) = point;
665
if x < self.width && y < self.height {
666
self.canvas[y][x] = symbol;
667
}
668
}
669
670
/// Draws a line of `length` from an `origin` along the `orientation`
671
fn draw_line(&mut self, origin: Point, orientation: Orientation, length: usize) {
672
let Point(x, y) = origin;
673
if let Orientation::Vertical = orientation {
674
let mut down = 0;
675
while down < length {
676
self.draw_symbol(Point(x, y + down), self.glyphs.vertical_line);
677
down += 1;
678
}
679
} else if let Orientation::Horizontal = orientation {
680
let mut right = 0;
681
while right < length {
682
self.draw_symbol(Point(x + right, y), self.glyphs.horizontal_line);
683
right += 1;
684
}
685
}
686
}
687
688
/// Draws a box of `width` and `height` with an `origin` being the top left corner
689
fn draw_box(&mut self, origin: Point, width: usize, height: usize) {
690
let Point(x, y) = origin;
691
self.draw_symbol(origin, self.glyphs.top_left_corner);
692
self.draw_symbol(Point(x + width - 1, y), self.glyphs.top_right_corner);
693
self.draw_symbol(Point(x, y + height - 1), self.glyphs.bottom_left_corner);
694
self.draw_symbol(
695
Point(x + width - 1, y + height - 1),
696
self.glyphs.bottom_right_corner,
697
);
698
self.draw_line(Point(x + 1, y), Orientation::Horizontal, width - 2);
699
self.draw_line(
700
Point(x + 1, y + height - 1),
701
Orientation::Horizontal,
702
width - 2,
703
);
704
self.draw_line(Point(x, y + 1), Orientation::Vertical, height - 2);
705
self.draw_line(
706
Point(x + width - 1, y + 1),
707
Orientation::Vertical,
708
height - 2,
709
);
710
}
711
712
/// Draws a box of height `2 + text.len()` containing a left-aligned text
713
fn draw_label_centered(&mut self, center: Point, text: &[&str]) {
714
if !text.is_empty() {
715
let Point(x, y) = center;
716
let text_width = text.iter().map(|l| l.len()).max().unwrap();
717
let half_width = text_width / 2 + text_width % 2;
718
let half_height = text.len() / 2;
719
if x >= half_width + 2 && y > half_height {
720
self.draw_box(
721
Point(x - half_width - 2, y - half_height - 1),
722
text_width + 4,
723
text.len() + 2,
724
);
725
for (i, line) in text.iter().enumerate() {
726
for (j, c) in line.chars().enumerate() {
727
self.draw_symbol(Point(x - half_width + j, y - half_height + i), c);
728
}
729
}
730
}
731
}
732
}
733
734
/// Draws branched lines from a `Point` to multiple `Point`s below
735
/// NOTE: the shape of these connections is very specific for this particular kind of the
736
/// representation of a tree
737
fn draw_connections(&mut self, from: Point, to: &[Point], branching_offset: usize) {
738
let mut start_with_corner = true;
739
let Point(mut x_from, mut y_from) = from;
740
for (i, Point(x, y)) in to.iter().enumerate() {
741
if *x >= x_from && *y >= y_from - 1 {
742
self.draw_symbol(Point(*x, *y), self.glyphs.tee_up);
743
if *x == x_from {
744
// if the first connection goes straight below
745
self.draw_symbol(Point(x_from, y_from - 1), self.glyphs.tee_down);
746
self.draw_line(Point(x_from, y_from), Orientation::Vertical, *y - y_from);
747
x_from += 1;
748
} else {
749
if start_with_corner {
750
// if the first or the second connection steers to the right
751
self.draw_symbol(Point(x_from, y_from - 1), self.glyphs.tee_down);
752
self.draw_line(
753
Point(x_from, y_from),
754
Orientation::Vertical,
755
branching_offset,
756
);
757
y_from += branching_offset;
758
self.draw_symbol(Point(x_from, y_from), self.glyphs.bottom_left_corner);
759
start_with_corner = false;
760
x_from += 1;
761
}
762
let length = *x - x_from;
763
self.draw_line(Point(x_from, y_from), Orientation::Horizontal, length);
764
x_from += length;
765
if i == to.len() - 1 {
766
self.draw_symbol(Point(x_from, y_from), self.glyphs.top_right_corner);
767
} else {
768
self.draw_symbol(Point(x_from, y_from), self.glyphs.tee_down);
769
}
770
self.draw_line(
771
Point(x_from, y_from + 1),
772
Orientation::Vertical,
773
*y - y_from - 1,
774
);
775
x_from += 1;
776
}
777
}
778
}
779
}
780
}
781
782
/// The actual drawing happens in the conversion of the intermediate `TreeView` into `Canvas`
783
impl From<TreeView<'_>> for Canvas {
784
fn from(value: TreeView<'_>) -> Self {
785
let width = value.n_rows_width + 3 + value.columns.iter().map(|c| c.width).sum::<usize>();
786
let height =
787
3 + value.rows.iter().map(|r| r.height).sum::<usize>() + 3 * (value.n_rows - 1);
788
let mut canvas = Canvas::new(width, height, Glyphs::default());
789
790
// Axles
791
let (x, y) = (value.n_rows_width + 2, 1);
792
canvas.draw_symbol(Point(x, y), '┌');
793
canvas.draw_line(Point(x + 1, y), Orientation::Horizontal, width - x);
794
canvas.draw_line(Point(x, y + 1), Orientation::Vertical, height - y);
795
796
// Row and column indices
797
for (i, row) in value.rows.iter().enumerate() {
798
// the prefix `Vec` of spaces compensates for the row indices that are shorter than the
799
// highest index, effectively, row indices are right-aligned
800
for (j, c) in vec![' '; value.n_rows_width - digits(i)]
801
.into_iter()
802
.chain(format!("{i}").chars())
803
.enumerate()
804
{
805
canvas.draw_symbol(Point(j + 1, row.offset + row.center), c);
806
}
807
}
808
for (j, col) in value.columns.iter().enumerate() {
809
let j_width = digits(j);
810
let start = col.offset + col.center - (j_width / 2 + j_width % 2);
811
for (k, c) in format!("{j}").chars().enumerate() {
812
canvas.draw_symbol(Point(start + k, 0), c);
813
}
814
}
815
816
// Non-empty cells (nodes) and their connections (edges)
817
for (i, row) in value.matrix.iter().enumerate() {
818
for (j, cell) in row.iter().enumerate() {
819
if !cell.text.is_empty() {
820
canvas.draw_label_centered(
821
Point(
822
value.columns[j].offset + value.columns[j].center,
823
value.rows[i].offset + value.rows[i].center,
824
),
825
&cell.text,
826
);
827
}
828
}
829
}
830
831
fn even_odd(a: usize, b: usize) -> usize {
832
if a.is_multiple_of(2) && b % 2 == 1 {
833
1
834
} else {
835
0
836
}
837
}
838
839
for (i, row) in value.matrix.iter().enumerate() {
840
for (j, cell) in row.iter().enumerate() {
841
if !cell.text.is_empty() && i < value.rows.len() - 1 {
842
let children_points = cell
843
.children_columns
844
.iter()
845
.map(|k| {
846
let child_total_padding =
847
value.rows[i + 1].height - value.matrix[i + 1][*k].text.len() - 2;
848
let even_cell_in_odd_row = even_odd(
849
value.matrix[i + 1][*k].text.len(),
850
value.rows[i + 1].height,
851
);
852
Point(
853
value.columns[*k].offset + value.columns[*k].center - 1,
854
value.rows[i + 1].offset
855
+ child_total_padding / 2
856
+ child_total_padding % 2
857
- even_cell_in_odd_row,
858
)
859
})
860
.collect::<Vec<_>>();
861
862
let parent_total_padding =
863
value.rows[i].height - value.matrix[i][j].text.len() - 2;
864
let even_cell_in_odd_row =
865
even_odd(value.matrix[i][j].text.len(), value.rows[i].height);
866
867
canvas.draw_connections(
868
Point(
869
value.columns[j].offset + value.columns[j].center - 1,
870
value.rows[i].offset + value.rows[i].height
871
- parent_total_padding / 2
872
- even_cell_in_odd_row,
873
),
874
&children_points,
875
parent_total_padding / 2 + 1 + even_cell_in_odd_row,
876
);
877
}
878
}
879
}
880
881
canvas
882
}
883
}
884
885
impl fmt::Display for Canvas {
886
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
887
for row in &self.canvas {
888
writeln!(f, "{}", row.iter().collect::<String>().trim_end())?;
889
}
890
891
Ok(())
892
}
893
}
894
895
fn tree_fmt_text(tree: &TreeFmtVisitor, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
896
let tree_view: TreeView<'_> = tree.levels.as_slice().into();
897
let canvas: Canvas = tree_view.into();
898
write!(f, "{canvas}")?;
899
900
Ok(())
901
}
902
903
// GraphViz Output
904
// Create a simple DOT graph String from TreeFmtVisitor
905
fn tree_fmt_dot(tree: &TreeFmtVisitor, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
906
// Build a dot graph as a string
907
let tree_view: TreeView<'_> = tree.levels.as_slice().into();
908
let mut relations: Vec<String> = Vec::new();
909
910
// Non-empty cells (nodes) and their connections (edges)
911
for (i, row) in tree_view.matrix.iter().enumerate() {
912
for (j, cell) in row.iter().enumerate() {
913
if !cell.text.is_empty() {
914
// Add node
915
let node_label = &cell.text.join("\n");
916
let node_desc = format!("n{i}{j} [label=\"{node_label}\", ordering=\"out\"]");
917
relations.push(node_desc);
918
919
// Add child edges
920
if i < tree_view.rows.len() - 1 {
921
// Iter in reversed order to undo the reversed child order when iterating expressions
922
for child_col in cell.children_columns.iter().rev() {
923
let next_row = i + 1;
924
let edge = format!("n{i}{j} -- n{next_row}{child_col}");
925
relations.push(edge);
926
}
927
}
928
}
929
}
930
}
931
932
let graph_str = relations.join("\n ");
933
let s = format!("graph {{\n {graph_str}\n}}");
934
write!(f, "{s}")?;
935
Ok(())
936
}
937
938
fn tree_fmt(tree: &TreeFmtVisitor, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
939
match tree.display {
940
TreeFmtVisitorDisplay::DisplayText => tree_fmt_text(tree, f),
941
TreeFmtVisitorDisplay::DisplayDot => tree_fmt_dot(tree, f),
942
}
943
}
944
945
impl fmt::Display for TreeFmtVisitor {
946
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
947
tree_fmt(self, f)
948
}
949
}
950
951
impl fmt::Debug for TreeFmtVisitor {
952
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
953
tree_fmt(self, f)
954
}
955
}
956
957