Path: blob/main/crates/polars-plan/src/plans/ir/tree_format.rs
8431 views
use std::fmt::{self, Write};12use polars_core::error::*;3use polars_utils::format_list_truncated;45use crate::constants;6use crate::plans::ir::IRPlanRef;7use crate::plans::visitor::{VisitRecursion, Visitor};8use crate::prelude::ir::format::ColumnsDisplay;9use crate::prelude::visitor::AexprNode;10use crate::prelude::*;1112pub struct TreeFmtNode<'a> {13h: Option<String>,14content: TreeFmtNodeContent<'a>,1516lp: IRPlanRef<'a>,17}1819pub struct TreeFmtAExpr<'a>(&'a AExpr);2021/// Hack UpperExpr trait to get a kind of formatting that doesn't traverse the nodes.22/// So we can format with {foo:E}23impl fmt::Display for TreeFmtAExpr<'_> {24fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {25let s = match self.0 {26AExpr::Element => "element()",27AExpr::Explode { expr: _, options } => {28f.write_str("explode(")?;29match (options.empty_as_null, options.keep_nulls) {30(true, true) => {},31(true, false) => f.write_str("keep_nulls=false")?,32(false, true) => f.write_str("empty_as_null=false")?,33(false, false) => f.write_str("empty_as_null=false, keep_nulls=false")?,34}35return f.write_char(')');36},37AExpr::Column(name) => return write!(f, "col({name})"),38#[cfg(feature = "dtype-struct")]39AExpr::StructField(name) => return write!(f, "field({name})"),40AExpr::Literal(lv) => return write!(f, "lit({lv:?})"),41AExpr::BinaryExpr { op, .. } => return write!(f, "binary: {op}"),42AExpr::Cast { dtype, options, .. } => {43return if options.is_strict() {44write!(f, "strict cast({dtype})")45} else {46write!(f, "cast({dtype})")47};48},49AExpr::Sort { options, .. } => {50return write!(51f,52"sort: {}{}{}",53options.descending as u8, options.nulls_last as u8, options.multithreaded as u854);55},56AExpr::Gather { .. } => "gather",57AExpr::SortBy { sort_options, .. } => {58write!(f, "sort_by:")?;59for i in &sort_options.descending {60write!(f, "{}", *i as u8)?;61}62for i in &sort_options.nulls_last {63write!(f, "{}", *i as u8)?;64}65write!(f, "{}", sort_options.multithreaded as u8)?;66return Ok(());67},68AExpr::Filter { .. } => "filter",69AExpr::Agg(a) => {70let s: &str = a.into();71return write!(f, "{}", s.to_lowercase());72},73AExpr::Ternary { .. } => "ternary",74AExpr::AnonymousFunction { fmt_str, .. } => {75return write!(f, "anonymous_function: {fmt_str}");76},77AExpr::AnonymousAgg { fmt_str, .. } => {78return write!(f, "anonymous_agg: {fmt_str}");79},80AExpr::Eval { .. } => "list.eval",81#[cfg(feature = "dtype-struct")]82AExpr::StructEval { .. } => "struct.with_fields",83AExpr::Function { function, .. } => return write!(f, "function: {function}"),84#[cfg(feature = "dynamic_group_by")]85AExpr::Rolling { .. } => "rolling",86AExpr::Over { .. } => "window",87AExpr::Slice { .. } => "slice",88AExpr::Len => constants::LEN,89};9091write!(f, "{s}")92}93}9495pub enum TreeFmtNodeContent<'a> {96Expression(&'a ExprIR),97LogicalPlan(Node),98}99100struct TreeFmtNodeData<'a>(String, Vec<TreeFmtNode<'a>>);101102fn with_header(header: &Option<String>, text: &str) -> String {103if let Some(header) = header {104format!("{header}\n{text}")105} else {106text.to_string()107}108}109110#[cfg(feature = "regex")]111fn multiline_expression(expr: &str) -> std::borrow::Cow<'_, str> {112polars_utils::regex_cache::cached_regex! {113static RE = r"([\)\]])(\.[a-z0-9]+\()";114}115RE.replace_all(expr, "$1\n $2")116}117118impl<'a> TreeFmtNode<'a> {119pub fn root_logical_plan(lp: IRPlanRef<'a>) -> Self {120Self {121h: None,122content: TreeFmtNodeContent::LogicalPlan(lp.lp_top),123124lp,125}126}127128pub fn lp_node(&self, h: Option<String>, root: Node) -> Self {129Self {130h,131content: TreeFmtNodeContent::LogicalPlan(root),132133lp: self.lp,134}135}136137pub fn expr_node(&self, h: Option<String>, expr: &'a ExprIR) -> Self {138Self {139h,140content: TreeFmtNodeContent::Expression(expr),141142lp: self.lp,143}144}145146pub fn traverse(&self, visitor: &mut TreeFmtVisitor) {147let TreeFmtNodeData(title, child_nodes) = self.node_data();148149if visitor.levels.len() <= visitor.depth {150visitor.levels.push(vec![]);151}152153let row = visitor.levels.get_mut(visitor.depth).unwrap();154row.resize(visitor.width + 1, "".to_string());155156row[visitor.width] = title;157visitor.prev_depth = visitor.depth;158visitor.depth += 1;159160for child in &child_nodes {161child.traverse(visitor);162}163164visitor.depth -= 1;165visitor.width += if visitor.prev_depth == visitor.depth {1661167} else {1680169};170}171172fn node_data(&self) -> TreeFmtNodeData<'_> {173use {TreeFmtNodeContent as C, TreeFmtNodeData as ND, with_header as wh};174175let lp = &self.lp;176let h = &self.h;177178use IR::*;179match self.content {180#[cfg(feature = "regex")]181C::Expression(expr) => ND(182wh(183h,184&multiline_expression(&expr.display(self.lp.expr_arena).to_string()),185),186vec![],187),188#[cfg(not(feature = "regex"))]189C::Expression(expr) => ND(wh(h, &expr.display(self.lp.expr_arena).to_string()), vec![]),190C::LogicalPlan(lp_top) => {191match self.lp.with_root(lp_top).root() {192#[cfg(feature = "python")]193PythonScan { .. } => ND(wh(h, &lp.describe()), vec![]),194Scan { .. } => ND(wh(h, &lp.describe()), vec![]),195DataFrameScan {196schema,197output_schema,198..199} => {200let (n_columns, projected) = if let Some(schema) = output_schema {201(202format!("{}", schema.len()),203format!(204": {};",205format_list_truncated!(schema.iter_names(), 4, '"')206),207)208} else {209("*".to_string(), "".to_string())210};211ND(212wh(213h,214&format!(215"DF {}\nPROJECT{} {}/{} COLUMNS",216format_list_truncated!(schema.iter_names(), 4, '"'),217projected,218n_columns,219schema.len()220),221),222vec![],223)224},225226Union { inputs, .. } => ND(227wh(228h,229// THis is commented out, but must be restored when we convert to IR's.230// &(if let Some(slice) = options.slice {231// format!("SLICED UNION: {slice:?}")232// } else {233// "UNION".to_string()234// }),235"UNION",236),237inputs238.iter()239.enumerate()240.map(|(i, lp_root)| self.lp_node(Some(format!("PLAN {i}:")), *lp_root))241.collect(),242),243HConcat { inputs, .. } => ND(244wh(h, "HCONCAT"),245inputs246.iter()247.enumerate()248.map(|(i, lp_root)| self.lp_node(Some(format!("PLAN {i}:")), *lp_root))249.collect(),250),251Cache { input, id } => ND(252wh(h, &format!("CACHE[id: {id}]")),253vec![self.lp_node(None, *input)],254),255Filter { input, predicate } => ND(256wh(h, "FILTER"),257vec![258self.expr_node(Some("predicate:".to_string()), predicate),259self.lp_node(Some("FROM:".to_string()), *input),260],261),262Select { expr, input, .. } => ND(263wh(h, "SELECT"),264expr.iter()265.map(|expr| self.expr_node(Some("expression:".to_string()), expr))266.chain([self.lp_node(Some("FROM:".to_string()), *input)])267.collect(),268),269Sort {270input, by_column, ..271} => ND(272wh(h, "SORT BY"),273by_column274.iter()275.map(|expr| self.expr_node(Some("expression:".to_string()), expr))276.chain([self.lp_node(None, *input)])277.collect(),278),279GroupBy {280input,281keys,282aggs,283maintain_order,284..285} => ND(286wh(287h,288&format!("AGGREGATE[maintain_order: {}]", *maintain_order),289),290aggs.iter()291.map(|expr| self.expr_node(Some("expression:".to_string()), expr))292.chain(keys.iter().map(|expr| {293self.expr_node(Some("aggregate by:".to_string()), expr)294}))295.chain([self.lp_node(Some("FROM:".to_string()), *input)])296.collect(),297),298Join {299input_left,300input_right,301left_on,302right_on,303options,304..305} => ND(306wh(h, &format!("{} JOIN", options.args.how)),307left_on308.iter()309.map(|expr| self.expr_node(Some("left on:".to_string()), expr))310.chain([self.lp_node(Some("LEFT PLAN:".to_string()), *input_left)])311.chain(312right_on.iter().map(|expr| {313self.expr_node(Some("right on:".to_string()), expr)314}),315)316.chain([self.lp_node(Some("RIGHT PLAN:".to_string()), *input_right)])317.collect(),318),319HStack { input, exprs, .. } => ND(320wh(h, "WITH_COLUMNS"),321exprs322.iter()323.map(|expr| self.expr_node(Some("expression:".to_string()), expr))324.chain([self.lp_node(None, *input)])325.collect(),326),327Distinct { input, options } => ND(328wh(329h,330&format!(331"UNIQUE[maintain_order: {:?}, keep_strategy: {:?}] BY {:?}",332options.maintain_order, options.keep_strategy, options.subset333),334),335vec![self.lp_node(None, *input)],336),337Slice { input, offset, len } => ND(338wh(h, &format!("SLICE[offset: {offset}, len: {len}]")),339vec![self.lp_node(None, *input)],340),341MapFunction { input, function } => ND(342wh(h, &format!("{function}")),343vec![self.lp_node(None, *input)],344),345ExtContext { input, .. } => {346ND(wh(h, "EXTERNAL_CONTEXT"), vec![self.lp_node(None, *input)])347},348Sink { input, payload } => ND(349wh(350h,351match payload {352SinkTypeIR::Memory => "SINK (memory)",353SinkTypeIR::Callback(..) => "SINK (callback)",354SinkTypeIR::File { .. } => "SINK (file)",355SinkTypeIR::Partitioned { .. } => "SINK (partition)",356},357),358vec![self.lp_node(None, *input)],359),360SinkMultiple { inputs } => ND(361wh(h, "SINK_MULTIPLE"),362inputs363.iter()364.enumerate()365.map(|(i, lp_root)| self.lp_node(Some(format!("PLAN {i}:")), *lp_root))366.collect(),367),368SimpleProjection { input, columns } => {369let num_columns = columns.as_ref().len();370let total_columns = lp.lp_arena.get(*input).schema(lp.lp_arena).len();371372let columns = ColumnsDisplay(columns.as_ref());373ND(374wh(375h,376&format!("simple π {num_columns}/{total_columns} [{columns}]"),377),378vec![self.lp_node(None, *input)],379)380},381#[cfg(feature = "merge_sorted")]382MergeSorted {383input_left,384input_right,385key,386} => ND(387wh(h, &format!("MERGE SORTED ON '{key}")),388[self.lp_node(Some("LEFT PLAN:".to_string()), *input_left)]389.into_iter()390.chain([self.lp_node(Some("RIGHT PLAN:".to_string()), *input_right)])391.collect(),392),393Invalid => ND(wh(h, "INVALID"), vec![]),394}395},396}397}398}399400#[derive(Default)]401pub enum TreeFmtVisitorDisplay {402#[default]403DisplayText,404DisplayDot,405}406407#[derive(Default)]408pub(crate) struct TreeFmtVisitor {409levels: Vec<Vec<String>>,410prev_depth: usize,411depth: usize,412width: usize,413pub(crate) display: TreeFmtVisitorDisplay,414}415416impl Visitor for TreeFmtVisitor {417type Node = AexprNode;418type Arena = Arena<AExpr>;419420/// Invoked before any children of `node` are visited.421fn pre_visit(422&mut self,423node: &Self::Node,424arena: &Self::Arena,425) -> PolarsResult<VisitRecursion> {426let repr = TreeFmtAExpr(arena.get(node.node()));427let repr = repr.to_string();428429if self.levels.len() <= self.depth {430self.levels.push(vec![])431}432433// the post-visit ensures the width of this node is known434let row = self.levels.get_mut(self.depth).unwrap();435436// set default values to ensure we format at the right width437row.resize(self.width + 1, "".to_string());438row[self.width] = repr;439440// before entering a depth-first branch we preserve the depth to control the width increase441// in the post-visit442self.prev_depth = self.depth;443444// we will enter depth first, we enter child so depth increases445self.depth += 1;446447Ok(VisitRecursion::Continue)448}449450fn post_visit(451&mut self,452_node: &Self::Node,453_arena: &Self::Arena,454) -> PolarsResult<VisitRecursion> {455// we finished this branch so we decrease in depth, back the caller node456self.depth -= 1;457458// because we traverse depth first459// the width is increased once after one or more depth-first branches460// this way we avoid empty columns in the resulting tree representation461self.width += if self.prev_depth == self.depth { 1 } else { 0 };462463Ok(VisitRecursion::Continue)464}465}466467/// Calculates the number of digits in a `usize` number468/// Useful for the alignment of `usize` values when they are displayed469fn digits(n: usize) -> usize {470if n == 0 {4711472} else {473f64::log10(n as f64) as usize + 1474}475}476477/// Meta-info of a column in a populated `TreeFmtVisitor` required for the pretty-print of a tree478#[derive(Clone, Default, Debug)]479struct TreeViewColumn {480offset: usize,481width: usize,482center: usize,483}484485/// Meta-info of a column in a populated `TreeFmtVisitor` required for the pretty-print of a tree486#[derive(Clone, Default, Debug)]487struct TreeViewRow {488offset: usize,489height: usize,490center: usize,491}492493/// Meta-info of a cell in a populated `TreeFmtVisitor`494#[derive(Clone, Default, Debug)]495struct TreeViewCell<'a> {496text: Vec<&'a str>,497/// A `Vec` of indices of `TreeViewColumn`-s stored elsewhere in another `Vec`498/// For a cell on a row `i` these indices point to the columns that contain child-cells on a499/// row `i + 1` (if the latter exists)500/// NOTE: might warrant a rethink should this code become used broader501children_columns: Vec<usize>,502}503504/// The complete intermediate representation of a `TreeFmtVisitor` that can be drawn on a `Canvas`505/// down the line506#[derive(Default, Debug)]507struct TreeView<'a> {508n_rows: usize,509n_rows_width: usize,510matrix: Vec<Vec<TreeViewCell<'a>>>,511/// NOTE: `TreeViewCell`'s `children_columns` field contains indices pointing at the elements512/// of this `Vec`513columns: Vec<TreeViewColumn>,514rows: Vec<TreeViewRow>,515}516517// NOTE: the code below this line is full of hardcoded integer offsets which may not be a big518// problem as long as it remains the private implementation of the pretty-print519/// The conversion from a reference to `levels` field of a `TreeFmtVisitor`520impl<'a> From<&'a [Vec<String>]> for TreeView<'a> {521#[allow(clippy::needless_range_loop)]522fn from(value: &'a [Vec<String>]) -> Self {523let n_rows = value.len();524let n_cols = value.iter().map(|row| row.len()).max().unwrap_or(0);525if n_rows == 0 || n_cols == 0 {526return TreeView::default();527}528// the character-width of the highest index of a row529let n_rows_width = digits(n_rows - 1);530531let mut matrix = vec![vec![TreeViewCell::default(); n_cols]; n_rows];532for i in 0..n_rows {533for j in 0..n_cols {534if j < value[i].len() && !value[i][j].is_empty() {535matrix[i][j].text = value[i][j].split('\n').collect();536if i < n_rows - 1 {537if j < value[i + 1].len() && !value[i + 1][j].is_empty() {538matrix[i][j].children_columns.push(j);539}540for k in j + 1..n_cols {541if (k >= value[i].len() || value[i][k].is_empty())542&& k < value[i + 1].len()543{544if !value[i + 1][k].is_empty() {545matrix[i][j].children_columns.push(k);546}547} else {548break;549}550}551}552}553}554}555556let mut y_offset = 3;557let mut rows = vec![TreeViewRow::default(); n_rows];558for i in 0..n_rows {559let mut height = 0;560for j in 0..n_cols {561height = [matrix[i][j].text.len(), height].into_iter().max().unwrap();562}563height += 2;564rows[i].offset = y_offset;565rows[i].height = height;566rows[i].center = height / 2;567y_offset += height + 3;568}569570let mut x_offset = n_rows_width + 4;571let mut columns = vec![TreeViewColumn::default(); n_cols];572// the two nested loops below are those `needless_range_loop`s573// more readable this way to my taste574for j in 0..n_cols {575let mut width = 0;576for i in 0..n_rows {577width = [578matrix[i][j].text.iter().map(|l| l.len()).max().unwrap_or(0),579width,580]581.into_iter()582.max()583.unwrap();584}585width += 6;586columns[j].offset = x_offset;587columns[j].width = width;588columns[j].center = width / 2 + width % 2;589x_offset += width;590}591592Self {593n_rows,594n_rows_width,595matrix,596columns,597rows,598}599}600}601602/// The basic charset that's used for drawing lines and boxes on a `Canvas`603struct Glyphs {604void: char,605vertical_line: char,606horizontal_line: char,607top_left_corner: char,608top_right_corner: char,609bottom_left_corner: char,610bottom_right_corner: char,611tee_down: char,612tee_up: char,613}614615impl Default for Glyphs {616fn default() -> Self {617Self {618void: ' ',619vertical_line: '│',620horizontal_line: '─',621top_left_corner: '╭',622top_right_corner: '╮',623bottom_left_corner: '╰',624bottom_right_corner: '╯',625tee_down: '┬',626tee_up: '┴',627}628}629}630631/// A `Point` on a `Canvas`632#[derive(Clone, Copy)]633struct Point(usize, usize);634635/// The orientation of a line on a `Canvas`636#[derive(Clone, Copy)]637enum Orientation {638Vertical,639Horizontal,640}641642/// `Canvas`643struct Canvas {644width: usize,645height: usize,646canvas: Vec<Vec<char>>,647glyphs: Glyphs,648}649650impl Canvas {651fn new(width: usize, height: usize, glyphs: Glyphs) -> Self {652Self {653width,654height,655canvas: vec![vec![glyphs.void; width]; height],656glyphs,657}658}659660/// Draws a single `symbol` on the `Canvas`661/// NOTE: The `Point`s that lay outside of the `Canvas` are quietly ignored662fn draw_symbol(&mut self, point: Point, symbol: char) {663let Point(x, y) = point;664if x < self.width && y < self.height {665self.canvas[y][x] = symbol;666}667}668669/// Draws a line of `length` from an `origin` along the `orientation`670fn draw_line(&mut self, origin: Point, orientation: Orientation, length: usize) {671let Point(x, y) = origin;672if let Orientation::Vertical = orientation {673let mut down = 0;674while down < length {675self.draw_symbol(Point(x, y + down), self.glyphs.vertical_line);676down += 1;677}678} else if let Orientation::Horizontal = orientation {679let mut right = 0;680while right < length {681self.draw_symbol(Point(x + right, y), self.glyphs.horizontal_line);682right += 1;683}684}685}686687/// Draws a box of `width` and `height` with an `origin` being the top left corner688fn draw_box(&mut self, origin: Point, width: usize, height: usize) {689let Point(x, y) = origin;690self.draw_symbol(origin, self.glyphs.top_left_corner);691self.draw_symbol(Point(x + width - 1, y), self.glyphs.top_right_corner);692self.draw_symbol(Point(x, y + height - 1), self.glyphs.bottom_left_corner);693self.draw_symbol(694Point(x + width - 1, y + height - 1),695self.glyphs.bottom_right_corner,696);697self.draw_line(Point(x + 1, y), Orientation::Horizontal, width - 2);698self.draw_line(699Point(x + 1, y + height - 1),700Orientation::Horizontal,701width - 2,702);703self.draw_line(Point(x, y + 1), Orientation::Vertical, height - 2);704self.draw_line(705Point(x + width - 1, y + 1),706Orientation::Vertical,707height - 2,708);709}710711/// Draws a box of height `2 + text.len()` containing a left-aligned text712fn draw_label_centered(&mut self, center: Point, text: &[&str]) {713if !text.is_empty() {714let Point(x, y) = center;715let text_width = text.iter().map(|l| l.len()).max().unwrap();716let half_width = text_width / 2 + text_width % 2;717let half_height = text.len() / 2;718if x >= half_width + 2 && y > half_height {719self.draw_box(720Point(x - half_width - 2, y - half_height - 1),721text_width + 4,722text.len() + 2,723);724for (i, line) in text.iter().enumerate() {725for (j, c) in line.chars().enumerate() {726self.draw_symbol(Point(x - half_width + j, y - half_height + i), c);727}728}729}730}731}732733/// Draws branched lines from a `Point` to multiple `Point`s below734/// NOTE: the shape of these connections is very specific for this particular kind of the735/// representation of a tree736fn draw_connections(&mut self, from: Point, to: &[Point], branching_offset: usize) {737let mut start_with_corner = true;738let Point(mut x_from, mut y_from) = from;739for (i, Point(x, y)) in to.iter().enumerate() {740if *x >= x_from && *y >= y_from - 1 {741self.draw_symbol(Point(*x, *y), self.glyphs.tee_up);742if *x == x_from {743// if the first connection goes straight below744self.draw_symbol(Point(x_from, y_from - 1), self.glyphs.tee_down);745self.draw_line(Point(x_from, y_from), Orientation::Vertical, *y - y_from);746x_from += 1;747} else {748if start_with_corner {749// if the first or the second connection steers to the right750self.draw_symbol(Point(x_from, y_from - 1), self.glyphs.tee_down);751self.draw_line(752Point(x_from, y_from),753Orientation::Vertical,754branching_offset,755);756y_from += branching_offset;757self.draw_symbol(Point(x_from, y_from), self.glyphs.bottom_left_corner);758start_with_corner = false;759x_from += 1;760}761let length = *x - x_from;762self.draw_line(Point(x_from, y_from), Orientation::Horizontal, length);763x_from += length;764if i == to.len() - 1 {765self.draw_symbol(Point(x_from, y_from), self.glyphs.top_right_corner);766} else {767self.draw_symbol(Point(x_from, y_from), self.glyphs.tee_down);768}769self.draw_line(770Point(x_from, y_from + 1),771Orientation::Vertical,772*y - y_from - 1,773);774x_from += 1;775}776}777}778}779}780781/// The actual drawing happens in the conversion of the intermediate `TreeView` into `Canvas`782impl From<TreeView<'_>> for Canvas {783fn from(value: TreeView<'_>) -> Self {784let width = value.n_rows_width + 3 + value.columns.iter().map(|c| c.width).sum::<usize>();785let height =7863 + value.rows.iter().map(|r| r.height).sum::<usize>() + 3 * (value.n_rows - 1);787let mut canvas = Canvas::new(width, height, Glyphs::default());788789// Axles790let (x, y) = (value.n_rows_width + 2, 1);791canvas.draw_symbol(Point(x, y), '┌');792canvas.draw_line(Point(x + 1, y), Orientation::Horizontal, width - x);793canvas.draw_line(Point(x, y + 1), Orientation::Vertical, height - y);794795// Row and column indices796for (i, row) in value.rows.iter().enumerate() {797// the prefix `Vec` of spaces compensates for the row indices that are shorter than the798// highest index, effectively, row indices are right-aligned799for (j, c) in vec![' '; value.n_rows_width - digits(i)]800.into_iter()801.chain(format!("{i}").chars())802.enumerate()803{804canvas.draw_symbol(Point(j + 1, row.offset + row.center), c);805}806}807for (j, col) in value.columns.iter().enumerate() {808let j_width = digits(j);809let start = col.offset + col.center - (j_width / 2 + j_width % 2);810for (k, c) in format!("{j}").chars().enumerate() {811canvas.draw_symbol(Point(start + k, 0), c);812}813}814815// Non-empty cells (nodes) and their connections (edges)816for (i, row) in value.matrix.iter().enumerate() {817for (j, cell) in row.iter().enumerate() {818if !cell.text.is_empty() {819canvas.draw_label_centered(820Point(821value.columns[j].offset + value.columns[j].center,822value.rows[i].offset + value.rows[i].center,823),824&cell.text,825);826}827}828}829830fn even_odd(a: usize, b: usize) -> usize {831if a.is_multiple_of(2) && b % 2 == 1 {8321833} else {8340835}836}837838for (i, row) in value.matrix.iter().enumerate() {839for (j, cell) in row.iter().enumerate() {840if !cell.text.is_empty() && i < value.rows.len() - 1 {841let children_points = cell842.children_columns843.iter()844.map(|k| {845let child_total_padding =846value.rows[i + 1].height - value.matrix[i + 1][*k].text.len() - 2;847let even_cell_in_odd_row = even_odd(848value.matrix[i + 1][*k].text.len(),849value.rows[i + 1].height,850);851Point(852value.columns[*k].offset + value.columns[*k].center - 1,853value.rows[i + 1].offset854+ child_total_padding / 2855+ child_total_padding % 2856- even_cell_in_odd_row,857)858})859.collect::<Vec<_>>();860861let parent_total_padding =862value.rows[i].height - value.matrix[i][j].text.len() - 2;863let even_cell_in_odd_row =864even_odd(value.matrix[i][j].text.len(), value.rows[i].height);865866canvas.draw_connections(867Point(868value.columns[j].offset + value.columns[j].center - 1,869value.rows[i].offset + value.rows[i].height870- parent_total_padding / 2871- even_cell_in_odd_row,872),873&children_points,874parent_total_padding / 2 + 1 + even_cell_in_odd_row,875);876}877}878}879880canvas881}882}883884impl fmt::Display for Canvas {885fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {886for row in &self.canvas {887writeln!(f, "{}", row.iter().collect::<String>().trim_end())?;888}889890Ok(())891}892}893894fn tree_fmt_text(tree: &TreeFmtVisitor, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {895let tree_view: TreeView<'_> = tree.levels.as_slice().into();896let canvas: Canvas = tree_view.into();897write!(f, "{canvas}")?;898899Ok(())900}901902// GraphViz Output903// Create a simple DOT graph String from TreeFmtVisitor904fn tree_fmt_dot(tree: &TreeFmtVisitor, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {905// Build a dot graph as a string906let tree_view: TreeView<'_> = tree.levels.as_slice().into();907let mut relations: Vec<String> = Vec::new();908909// Non-empty cells (nodes) and their connections (edges)910for (i, row) in tree_view.matrix.iter().enumerate() {911for (j, cell) in row.iter().enumerate() {912if !cell.text.is_empty() {913// Add node914let node_label = &cell.text.join("\n");915let node_desc = format!("n{i}{j} [label=\"{node_label}\", ordering=\"out\"]");916relations.push(node_desc);917918// Add child edges919if i < tree_view.rows.len() - 1 {920// Iter in reversed order to undo the reversed child order when iterating expressions921for child_col in cell.children_columns.iter().rev() {922let next_row = i + 1;923let edge = format!("n{i}{j} -- n{next_row}{child_col}");924relations.push(edge);925}926}927}928}929}930931let graph_str = relations.join("\n ");932let s = format!("graph {{\n {graph_str}\n}}");933write!(f, "{s}")?;934Ok(())935}936937fn tree_fmt(tree: &TreeFmtVisitor, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {938match tree.display {939TreeFmtVisitorDisplay::DisplayText => tree_fmt_text(tree, f),940TreeFmtVisitorDisplay::DisplayDot => tree_fmt_dot(tree, f),941}942}943944impl fmt::Display for TreeFmtVisitor {945fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {946tree_fmt(self, f)947}948}949950impl fmt::Debug for TreeFmtVisitor {951fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {952tree_fmt(self, f)953}954}955956957