Path: blob/main/crates/polars-plan/src/plans/ir/tree_format.rs
6940 views
use std::fmt;12use polars_core::error::*;3use polars_utils::{format_list_container_truncated, 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::Explode {27expr: _,28skip_empty: false,29} => "explode",30AExpr::Explode {31expr: _,32skip_empty: true,33} => "explode(skip_empty)",34AExpr::Column(name) => return write!(f, "col({name})"),35AExpr::Literal(lv) => return write!(f, "lit({lv:?})"),36AExpr::BinaryExpr { op, .. } => return write!(f, "binary: {op}"),37AExpr::Cast { dtype, options, .. } => {38return if options.is_strict() {39write!(f, "strict cast({dtype})")40} else {41write!(f, "cast({dtype})")42};43},44AExpr::Sort { options, .. } => {45return write!(46f,47"sort: {}{}{}",48options.descending as u8, options.nulls_last as u8, options.multithreaded as u849);50},51AExpr::Gather { .. } => "gather",52AExpr::SortBy { sort_options, .. } => {53write!(f, "sort_by:")?;54for i in &sort_options.descending {55write!(f, "{}", *i as u8)?;56}57for i in &sort_options.nulls_last {58write!(f, "{}", *i as u8)?;59}60write!(f, "{}", sort_options.multithreaded as u8)?;61return Ok(());62},63AExpr::Filter { .. } => "filter",64AExpr::Agg(a) => {65let s: &str = a.into();66return write!(f, "{}", s.to_lowercase());67},68AExpr::Ternary { .. } => "ternary",69AExpr::AnonymousFunction { fmt_str, .. } => {70return write!(f, "anonymous_function: {fmt_str}");71},72AExpr::Eval { .. } => "list.eval",73AExpr::Function { function, .. } => return write!(f, "function: {function}"),74AExpr::Window { .. } => "window",75AExpr::Slice { .. } => "slice",76AExpr::Len => constants::LEN,77};7879write!(f, "{s}")80}81}8283pub enum TreeFmtNodeContent<'a> {84Expression(&'a ExprIR),85LogicalPlan(Node),86}8788struct TreeFmtNodeData<'a>(String, Vec<TreeFmtNode<'a>>);8990fn with_header(header: &Option<String>, text: &str) -> String {91if let Some(header) = header {92format!("{header}\n{text}")93} else {94text.to_string()95}96}9798#[cfg(feature = "regex")]99fn multiline_expression(expr: &str) -> std::borrow::Cow<'_, str> {100polars_utils::regex_cache::cached_regex! {101static RE = r"([\)\]])(\.[a-z0-9]+\()";102}103RE.replace_all(expr, "$1\n $2")104}105106impl<'a> TreeFmtNode<'a> {107pub fn root_logical_plan(lp: IRPlanRef<'a>) -> Self {108Self {109h: None,110content: TreeFmtNodeContent::LogicalPlan(lp.lp_top),111112lp,113}114}115116pub fn lp_node(&self, h: Option<String>, root: Node) -> Self {117Self {118h,119content: TreeFmtNodeContent::LogicalPlan(root),120121lp: self.lp,122}123}124125pub fn expr_node(&self, h: Option<String>, expr: &'a ExprIR) -> Self {126Self {127h,128content: TreeFmtNodeContent::Expression(expr),129130lp: self.lp,131}132}133134pub fn traverse(&self, visitor: &mut TreeFmtVisitor) {135let TreeFmtNodeData(title, child_nodes) = self.node_data();136137if visitor.levels.len() <= visitor.depth {138visitor.levels.push(vec![]);139}140141let row = visitor.levels.get_mut(visitor.depth).unwrap();142row.resize(visitor.width + 1, "".to_string());143144row[visitor.width] = title;145visitor.prev_depth = visitor.depth;146visitor.depth += 1;147148for child in &child_nodes {149child.traverse(visitor);150}151152visitor.depth -= 1;153visitor.width += if visitor.prev_depth == visitor.depth {1541155} else {1560157};158}159160fn node_data(&self) -> TreeFmtNodeData<'_> {161use {TreeFmtNodeContent as C, TreeFmtNodeData as ND, with_header as wh};162163let lp = &self.lp;164let h = &self.h;165166use IR::*;167match self.content {168#[cfg(feature = "regex")]169C::Expression(expr) => ND(170wh(171h,172&multiline_expression(&expr.display(self.lp.expr_arena).to_string()),173),174vec![],175),176#[cfg(not(feature = "regex"))]177C::Expression(expr) => ND(wh(h, &expr.display(self.lp.expr_arena).to_string()), vec![]),178C::LogicalPlan(lp_top) => {179match self.lp.with_root(lp_top).root() {180#[cfg(feature = "python")]181PythonScan { .. } => ND(wh(h, &lp.describe()), vec![]),182Scan { .. } => ND(wh(h, &lp.describe()), vec![]),183DataFrameScan {184schema,185output_schema,186..187} => {188let (n_columns, projected) = if let Some(schema) = output_schema {189(190format!("{}", schema.len()),191format!(192": {};",193format_list_truncated!(schema.iter_names(), 4, '"')194),195)196} else {197("*".to_string(), "".to_string())198};199ND(200wh(201h,202&format!(203"DF {}\nPROJECT{} {}/{} COLUMNS",204format_list_truncated!(schema.iter_names(), 4, '"'),205projected,206n_columns,207schema.len()208),209),210vec![],211)212},213214Union { inputs, .. } => ND(215wh(216h,217// THis is commented out, but must be restored when we convert to IR's.218// &(if let Some(slice) = options.slice {219// format!("SLICED UNION: {slice:?}")220// } else {221// "UNION".to_string()222// }),223"UNION",224),225inputs226.iter()227.enumerate()228.map(|(i, lp_root)| self.lp_node(Some(format!("PLAN {i}:")), *lp_root))229.collect(),230),231HConcat { inputs, .. } => ND(232wh(h, "HCONCAT"),233inputs234.iter()235.enumerate()236.map(|(i, lp_root)| self.lp_node(Some(format!("PLAN {i}:")), *lp_root))237.collect(),238),239Cache { input, id } => ND(240wh(h, &format!("CACHE[id: {id}]")),241vec![self.lp_node(None, *input)],242),243Filter { input, predicate } => ND(244wh(h, "FILTER"),245vec![246self.expr_node(Some("predicate:".to_string()), predicate),247self.lp_node(Some("FROM:".to_string()), *input),248],249),250Select { expr, input, .. } => ND(251wh(h, "SELECT"),252expr.iter()253.map(|expr| self.expr_node(Some("expression:".to_string()), expr))254.chain([self.lp_node(Some("FROM:".to_string()), *input)])255.collect(),256),257Sort {258input, by_column, ..259} => ND(260wh(h, "SORT BY"),261by_column262.iter()263.map(|expr| self.expr_node(Some("expression:".to_string()), expr))264.chain([self.lp_node(None, *input)])265.collect(),266),267GroupBy {268input,269keys,270aggs,271maintain_order,272..273} => ND(274wh(275h,276&format!("AGGREGATE[maintain_order: {}]", *maintain_order),277),278aggs.iter()279.map(|expr| self.expr_node(Some("expression:".to_string()), expr))280.chain(keys.iter().map(|expr| {281self.expr_node(Some("aggregate by:".to_string()), expr)282}))283.chain([self.lp_node(Some("FROM:".to_string()), *input)])284.collect(),285),286Join {287input_left,288input_right,289left_on,290right_on,291options,292..293} => ND(294wh(h, &format!("{} JOIN", options.args.how)),295left_on296.iter()297.map(|expr| self.expr_node(Some("left on:".to_string()), expr))298.chain([self.lp_node(Some("LEFT PLAN:".to_string()), *input_left)])299.chain(300right_on.iter().map(|expr| {301self.expr_node(Some("right on:".to_string()), expr)302}),303)304.chain([self.lp_node(Some("RIGHT PLAN:".to_string()), *input_right)])305.collect(),306),307HStack { input, exprs, .. } => ND(308wh(h, "WITH_COLUMNS"),309exprs310.iter()311.map(|expr| self.expr_node(Some("expression:".to_string()), expr))312.chain([self.lp_node(None, *input)])313.collect(),314),315Distinct { input, options } => ND(316wh(317h,318&format!(319"UNIQUE[maintain_order: {:?}, keep_strategy: {:?}] BY {:?}",320options.maintain_order, options.keep_strategy, options.subset321),322),323vec![self.lp_node(None, *input)],324),325Slice { input, offset, len } => ND(326wh(h, &format!("SLICE[offset: {offset}, len: {len}]")),327vec![self.lp_node(None, *input)],328),329MapFunction { input, function } => ND(330wh(h, &format!("{function}")),331vec![self.lp_node(None, *input)],332),333ExtContext { input, .. } => {334ND(wh(h, "EXTERNAL_CONTEXT"), vec![self.lp_node(None, *input)])335},336Sink { input, payload } => ND(337wh(338h,339match payload {340SinkTypeIR::Memory => "SINK (memory)",341SinkTypeIR::File { .. } => "SINK (file)",342SinkTypeIR::Partition { .. } => "SINK (partition)",343},344),345vec![self.lp_node(None, *input)],346),347SinkMultiple { inputs } => ND(348wh(h, "SINK_MULTIPLE"),349inputs350.iter()351.enumerate()352.map(|(i, lp_root)| self.lp_node(Some(format!("PLAN {i}:")), *lp_root))353.collect(),354),355SimpleProjection { input, columns } => {356let num_columns = columns.as_ref().len();357let total_columns = lp.lp_arena.get(*input).schema(lp.lp_arena).len();358359let columns = ColumnsDisplay(columns.as_ref());360ND(361wh(362h,363&format!("simple π {num_columns}/{total_columns} [{columns}]"),364),365vec![self.lp_node(None, *input)],366)367},368#[cfg(feature = "merge_sorted")]369MergeSorted {370input_left,371input_right,372key,373} => ND(374wh(h, &format!("MERGE SORTED ON '{key}")),375[self.lp_node(Some("LEFT PLAN:".to_string()), *input_left)]376.into_iter()377.chain([self.lp_node(Some("RIGHT PLAN:".to_string()), *input_right)])378.collect(),379),380Invalid => ND(wh(h, "INVALID"), vec![]),381}382},383}384}385}386387#[derive(Default)]388pub enum TreeFmtVisitorDisplay {389#[default]390DisplayText,391DisplayDot,392}393394#[derive(Default)]395pub(crate) struct TreeFmtVisitor {396levels: Vec<Vec<String>>,397prev_depth: usize,398depth: usize,399width: usize,400pub(crate) display: TreeFmtVisitorDisplay,401}402403impl Visitor for TreeFmtVisitor {404type Node = AexprNode;405type Arena = Arena<AExpr>;406407/// Invoked before any children of `node` are visited.408fn pre_visit(409&mut self,410node: &Self::Node,411arena: &Self::Arena,412) -> PolarsResult<VisitRecursion> {413let repr = TreeFmtAExpr(arena.get(node.node()));414let repr = repr.to_string();415416if self.levels.len() <= self.depth {417self.levels.push(vec![])418}419420// the post-visit ensures the width of this node is known421let row = self.levels.get_mut(self.depth).unwrap();422423// set default values to ensure we format at the right width424row.resize(self.width + 1, "".to_string());425row[self.width] = repr;426427// before entering a depth-first branch we preserve the depth to control the width increase428// in the post-visit429self.prev_depth = self.depth;430431// we will enter depth first, we enter child so depth increases432self.depth += 1;433434Ok(VisitRecursion::Continue)435}436437fn post_visit(438&mut self,439_node: &Self::Node,440_arena: &Self::Arena,441) -> PolarsResult<VisitRecursion> {442// we finished this branch so we decrease in depth, back the caller node443self.depth -= 1;444445// because we traverse depth first446// the width is increased once after one or more depth-first branches447// this way we avoid empty columns in the resulting tree representation448self.width += if self.prev_depth == self.depth { 1 } else { 0 };449450Ok(VisitRecursion::Continue)451}452}453454/// Calculates the number of digits in a `usize` number455/// Useful for the alignment of `usize` values when they are displayed456fn digits(n: usize) -> usize {457if n == 0 {4581459} else {460f64::log10(n as f64) as usize + 1461}462}463464/// Meta-info of a column in a populated `TreeFmtVisitor` required for the pretty-print of a tree465#[derive(Clone, Default, Debug)]466struct TreeViewColumn {467offset: usize,468width: usize,469center: usize,470}471472/// Meta-info of a column in a populated `TreeFmtVisitor` required for the pretty-print of a tree473#[derive(Clone, Default, Debug)]474struct TreeViewRow {475offset: usize,476height: usize,477center: usize,478}479480/// Meta-info of a cell in a populated `TreeFmtVisitor`481#[derive(Clone, Default, Debug)]482struct TreeViewCell<'a> {483text: Vec<&'a str>,484/// A `Vec` of indices of `TreeViewColumn`-s stored elsewhere in another `Vec`485/// For a cell on a row `i` these indices point to the columns that contain child-cells on a486/// row `i + 1` (if the latter exists)487/// NOTE: might warrant a rethink should this code become used broader488children_columns: Vec<usize>,489}490491/// The complete intermediate representation of a `TreeFmtVisitor` that can be drawn on a `Canvas`492/// down the line493#[derive(Default, Debug)]494struct TreeView<'a> {495n_rows: usize,496n_rows_width: usize,497matrix: Vec<Vec<TreeViewCell<'a>>>,498/// NOTE: `TreeViewCell`'s `children_columns` field contains indices pointing at the elements499/// of this `Vec`500columns: Vec<TreeViewColumn>,501rows: Vec<TreeViewRow>,502}503504// NOTE: the code below this line is full of hardcoded integer offsets which may not be a big505// problem as long as it remains the private implementation of the pretty-print506/// The conversion from a reference to `levels` field of a `TreeFmtVisitor`507impl<'a> From<&'a [Vec<String>]> for TreeView<'a> {508#[allow(clippy::needless_range_loop)]509fn from(value: &'a [Vec<String>]) -> Self {510let n_rows = value.len();511let n_cols = value.iter().map(|row| row.len()).max().unwrap_or(0);512if n_rows == 0 || n_cols == 0 {513return TreeView::default();514}515// the character-width of the highest index of a row516let n_rows_width = digits(n_rows - 1);517518let mut matrix = vec![vec![TreeViewCell::default(); n_cols]; n_rows];519for i in 0..n_rows {520for j in 0..n_cols {521if j < value[i].len() && !value[i][j].is_empty() {522matrix[i][j].text = value[i][j].split('\n').collect();523if i < n_rows - 1 {524if j < value[i + 1].len() && !value[i + 1][j].is_empty() {525matrix[i][j].children_columns.push(j);526}527for k in j + 1..n_cols {528if (k >= value[i].len() || value[i][k].is_empty())529&& k < value[i + 1].len()530{531if !value[i + 1][k].is_empty() {532matrix[i][j].children_columns.push(k);533}534} else {535break;536}537}538}539}540}541}542543let mut y_offset = 3;544let mut rows = vec![TreeViewRow::default(); n_rows];545for i in 0..n_rows {546let mut height = 0;547for j in 0..n_cols {548height = [matrix[i][j].text.len(), height].into_iter().max().unwrap();549}550height += 2;551rows[i].offset = y_offset;552rows[i].height = height;553rows[i].center = height / 2;554y_offset += height + 3;555}556557let mut x_offset = n_rows_width + 4;558let mut columns = vec![TreeViewColumn::default(); n_cols];559// the two nested loops below are those `needless_range_loop`s560// more readable this way to my taste561for j in 0..n_cols {562let mut width = 0;563for i in 0..n_rows {564width = [565matrix[i][j].text.iter().map(|l| l.len()).max().unwrap_or(0),566width,567]568.into_iter()569.max()570.unwrap();571}572width += 6;573columns[j].offset = x_offset;574columns[j].width = width;575columns[j].center = width / 2 + width % 2;576x_offset += width;577}578579Self {580n_rows,581n_rows_width,582matrix,583columns,584rows,585}586}587}588589/// The basic charset that's used for drawing lines and boxes on a `Canvas`590struct Glyphs {591void: char,592vertical_line: char,593horizontal_line: char,594top_left_corner: char,595top_right_corner: char,596bottom_left_corner: char,597bottom_right_corner: char,598tee_down: char,599tee_up: char,600}601602impl Default for Glyphs {603fn default() -> Self {604Self {605void: ' ',606vertical_line: '│',607horizontal_line: '─',608top_left_corner: '╭',609top_right_corner: '╮',610bottom_left_corner: '╰',611bottom_right_corner: '╯',612tee_down: '┬',613tee_up: '┴',614}615}616}617618/// A `Point` on a `Canvas`619#[derive(Clone, Copy)]620struct Point(usize, usize);621622/// The orientation of a line on a `Canvas`623#[derive(Clone, Copy)]624enum Orientation {625Vertical,626Horizontal,627}628629/// `Canvas`630struct Canvas {631width: usize,632height: usize,633canvas: Vec<Vec<char>>,634glyphs: Glyphs,635}636637impl Canvas {638fn new(width: usize, height: usize, glyphs: Glyphs) -> Self {639Self {640width,641height,642canvas: vec![vec![glyphs.void; width]; height],643glyphs,644}645}646647/// Draws a single `symbol` on the `Canvas`648/// NOTE: The `Point`s that lay outside of the `Canvas` are quietly ignored649fn draw_symbol(&mut self, point: Point, symbol: char) {650let Point(x, y) = point;651if x < self.width && y < self.height {652self.canvas[y][x] = symbol;653}654}655656/// Draws a line of `length` from an `origin` along the `orientation`657fn draw_line(&mut self, origin: Point, orientation: Orientation, length: usize) {658let Point(x, y) = origin;659if let Orientation::Vertical = orientation {660let mut down = 0;661while down < length {662self.draw_symbol(Point(x, y + down), self.glyphs.vertical_line);663down += 1;664}665} else if let Orientation::Horizontal = orientation {666let mut right = 0;667while right < length {668self.draw_symbol(Point(x + right, y), self.glyphs.horizontal_line);669right += 1;670}671}672}673674/// Draws a box of `width` and `height` with an `origin` being the top left corner675fn draw_box(&mut self, origin: Point, width: usize, height: usize) {676let Point(x, y) = origin;677self.draw_symbol(origin, self.glyphs.top_left_corner);678self.draw_symbol(Point(x + width - 1, y), self.glyphs.top_right_corner);679self.draw_symbol(Point(x, y + height - 1), self.glyphs.bottom_left_corner);680self.draw_symbol(681Point(x + width - 1, y + height - 1),682self.glyphs.bottom_right_corner,683);684self.draw_line(Point(x + 1, y), Orientation::Horizontal, width - 2);685self.draw_line(686Point(x + 1, y + height - 1),687Orientation::Horizontal,688width - 2,689);690self.draw_line(Point(x, y + 1), Orientation::Vertical, height - 2);691self.draw_line(692Point(x + width - 1, y + 1),693Orientation::Vertical,694height - 2,695);696}697698/// Draws a box of height `2 + text.len()` containing a left-aligned text699fn draw_label_centered(&mut self, center: Point, text: &[&str]) {700if !text.is_empty() {701let Point(x, y) = center;702let text_width = text.iter().map(|l| l.len()).max().unwrap();703let half_width = text_width / 2 + text_width % 2;704let half_height = text.len() / 2;705if x >= half_width + 2 && y > half_height {706self.draw_box(707Point(x - half_width - 2, y - half_height - 1),708text_width + 4,709text.len() + 2,710);711for (i, line) in text.iter().enumerate() {712for (j, c) in line.chars().enumerate() {713self.draw_symbol(Point(x - half_width + j, y - half_height + i), c);714}715}716}717}718}719720/// Draws branched lines from a `Point` to multiple `Point`s below721/// NOTE: the shape of these connections is very specific for this particular kind of the722/// representation of a tree723fn draw_connections(&mut self, from: Point, to: &[Point], branching_offset: usize) {724let mut start_with_corner = true;725let Point(mut x_from, mut y_from) = from;726for (i, Point(x, y)) in to.iter().enumerate() {727if *x >= x_from && *y >= y_from - 1 {728self.draw_symbol(Point(*x, *y), self.glyphs.tee_up);729if *x == x_from {730// if the first connection goes straight below731self.draw_symbol(Point(x_from, y_from - 1), self.glyphs.tee_down);732self.draw_line(Point(x_from, y_from), Orientation::Vertical, *y - y_from);733x_from += 1;734} else {735if start_with_corner {736// if the first or the second connection steers to the right737self.draw_symbol(Point(x_from, y_from - 1), self.glyphs.tee_down);738self.draw_line(739Point(x_from, y_from),740Orientation::Vertical,741branching_offset,742);743y_from += branching_offset;744self.draw_symbol(Point(x_from, y_from), self.glyphs.bottom_left_corner);745start_with_corner = false;746x_from += 1;747}748let length = *x - x_from;749self.draw_line(Point(x_from, y_from), Orientation::Horizontal, length);750x_from += length;751if i == to.len() - 1 {752self.draw_symbol(Point(x_from, y_from), self.glyphs.top_right_corner);753} else {754self.draw_symbol(Point(x_from, y_from), self.glyphs.tee_down);755}756self.draw_line(757Point(x_from, y_from + 1),758Orientation::Vertical,759*y - y_from - 1,760);761x_from += 1;762}763}764}765}766}767768/// The actual drawing happens in the conversion of the intermediate `TreeView` into `Canvas`769impl From<TreeView<'_>> for Canvas {770fn from(value: TreeView<'_>) -> Self {771let width = value.n_rows_width + 3 + value.columns.iter().map(|c| c.width).sum::<usize>();772let height =7733 + value.rows.iter().map(|r| r.height).sum::<usize>() + 3 * (value.n_rows - 1);774let mut canvas = Canvas::new(width, height, Glyphs::default());775776// Axles777let (x, y) = (value.n_rows_width + 2, 1);778canvas.draw_symbol(Point(x, y), '┌');779canvas.draw_line(Point(x + 1, y), Orientation::Horizontal, width - x);780canvas.draw_line(Point(x, y + 1), Orientation::Vertical, height - y);781782// Row and column indices783for (i, row) in value.rows.iter().enumerate() {784// the prefix `Vec` of spaces compensates for the row indices that are shorter than the785// highest index, effectively, row indices are right-aligned786for (j, c) in vec![' '; value.n_rows_width - digits(i)]787.into_iter()788.chain(format!("{i}").chars())789.enumerate()790{791canvas.draw_symbol(Point(j + 1, row.offset + row.center), c);792}793}794for (j, col) in value.columns.iter().enumerate() {795let j_width = digits(j);796let start = col.offset + col.center - (j_width / 2 + j_width % 2);797for (k, c) in format!("{j}").chars().enumerate() {798canvas.draw_symbol(Point(start + k, 0), c);799}800}801802// Non-empty cells (nodes) and their connections (edges)803for (i, row) in value.matrix.iter().enumerate() {804for (j, cell) in row.iter().enumerate() {805if !cell.text.is_empty() {806canvas.draw_label_centered(807Point(808value.columns[j].offset + value.columns[j].center,809value.rows[i].offset + value.rows[i].center,810),811&cell.text,812);813}814}815}816817fn even_odd(a: usize, b: usize) -> usize {818if a.is_multiple_of(2) && b % 2 == 1 {8191820} else {8210822}823}824825for (i, row) in value.matrix.iter().enumerate() {826for (j, cell) in row.iter().enumerate() {827if !cell.text.is_empty() && i < value.rows.len() - 1 {828let children_points = cell829.children_columns830.iter()831.map(|k| {832let child_total_padding =833value.rows[i + 1].height - value.matrix[i + 1][*k].text.len() - 2;834let even_cell_in_odd_row = even_odd(835value.matrix[i + 1][*k].text.len(),836value.rows[i + 1].height,837);838Point(839value.columns[*k].offset + value.columns[*k].center - 1,840value.rows[i + 1].offset841+ child_total_padding / 2842+ child_total_padding % 2843- even_cell_in_odd_row,844)845})846.collect::<Vec<_>>();847848let parent_total_padding =849value.rows[i].height - value.matrix[i][j].text.len() - 2;850let even_cell_in_odd_row =851even_odd(value.matrix[i][j].text.len(), value.rows[i].height);852853canvas.draw_connections(854Point(855value.columns[j].offset + value.columns[j].center - 1,856value.rows[i].offset + value.rows[i].height857- parent_total_padding / 2858- even_cell_in_odd_row,859),860&children_points,861parent_total_padding / 2 + 1 + even_cell_in_odd_row,862);863}864}865}866867canvas868}869}870871impl fmt::Display for Canvas {872fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {873for row in &self.canvas {874writeln!(f, "{}", row.iter().collect::<String>().trim_end())?;875}876877Ok(())878}879}880881fn tree_fmt_text(tree: &TreeFmtVisitor, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {882let tree_view: TreeView<'_> = tree.levels.as_slice().into();883let canvas: Canvas = tree_view.into();884write!(f, "{canvas}")?;885886Ok(())887}888889// GraphViz Output890// Create a simple DOT graph String from TreeFmtVisitor891fn tree_fmt_dot(tree: &TreeFmtVisitor, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {892// Build a dot graph as a string893let tree_view: TreeView<'_> = tree.levels.as_slice().into();894let mut relations: Vec<String> = Vec::new();895896// Non-empty cells (nodes) and their connections (edges)897for (i, row) in tree_view.matrix.iter().enumerate() {898for (j, cell) in row.iter().enumerate() {899if !cell.text.is_empty() {900// Add node901let node_label = &cell.text.join("\n");902let node_desc = format!("n{i}{j} [label=\"{node_label}\", ordering=\"out\"]");903relations.push(node_desc);904905// Add child edges906if i < tree_view.rows.len() - 1 {907// Iter in reversed order to undo the reversed child order when iterating expressions908for child_col in cell.children_columns.iter().rev() {909let next_row = i + 1;910let edge = format!("n{i}{j} -- n{next_row}{child_col}");911relations.push(edge);912}913}914}915}916}917918let graph_str = relations.join("\n ");919let s = format!("graph {{\n {graph_str}\n}}");920write!(f, "{s}")?;921Ok(())922}923924fn tree_fmt(tree: &TreeFmtVisitor, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {925match tree.display {926TreeFmtVisitorDisplay::DisplayText => tree_fmt_text(tree, f),927TreeFmtVisitorDisplay::DisplayDot => tree_fmt_dot(tree, f),928}929}930931impl fmt::Display for TreeFmtVisitor {932fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {933tree_fmt(self, f)934}935}936937impl fmt::Debug for TreeFmtVisitor {938fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {939tree_fmt(self, f)940}941}942943944