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