Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ descendleft
getroot
print_tree
printnode
nodevalue_distance
print_child_key
TreeCharSet
shouldprintkeys
Expand Down
83 changes: 70 additions & 13 deletions src/printing.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ Print a text representation of `tree` to the given `io` object.
* `io::IO` - IO stream to write to.
* `tree` - tree to print.
* `maxdepth::Integer = 5` - truncate printing of subtrees at this depth.
* `maxsibling::Union{Nothing,Integer} = nothing` - if set, print at most this many
consecutive similar siblings and then elide the rest of that run.
* `sibling_similarity_threshold::Real = 0` - threshold for considering sibling node values
similar. Non-numeric node values currently only support exact matching.
* `indicate_truncation::Bool = true` - print a vertical ellipsis character beneath
truncated nodes.
* `charset::TreeCharSet` - [`TreeCharSet`](@ref) to use to print branches.
Expand Down Expand Up @@ -191,15 +195,46 @@ print_child_key(io::IO, key::CartesianIndex) = show(io, Tuple(key))

branchwidth(cs::TreeCharSet) = sum(textwidth.((cs.mid, cs.dash)))


"""
nodevalue_distance(v1, v2; kw...)::Real

Compute the distance between the values of two child nodes. By default, equivalent to isequal.
It must return a Real

**OPTIONAL**: This can be extended for custom types and controls how nodes are shown
in [`print_tree`](@ref).
"""
nodevalue_distance(v1, v2) = ifelse(isequal(v1, v2), zero(Float64), Inf)

function print_and_child_prefix(io::IO, prefix::AbstractString, is_last::Bool, charset::TreeCharSet)
print(io, prefix)

if is_last
print(io, charset.terminator)
child_prefix = prefix * " " ^ (textwidth(charset.skip) + textwidth(charset.dash) + 1)
else
print(io, charset.mid)
child_prefix = prefix * charset.skip * " " ^ (textwidth(charset.dash) + 1)
end

print(io, charset.dash, ' ')

return child_prefix
end

function print_tree(printnode::Function, print_child_key::Function, io::IO, node;
maxdepth::Integer=5,
maxsibling::Union{Nothing,Integer}=nothing,
sibling_similarity_threshold::Real=0,
indicate_truncation::Bool=true,
charset::TreeCharSet=TreeCharSet(),
printkeys::Union{Bool,Nothing}=nothing,
depth::Integer=0,
prefix::AbstractString="",
printnode_kw=(;),
)

# Get node representation as string
buf = IOBuffer()
printnode(IOContext(buf, io), node; printnode_kw...)
Expand Down Expand Up @@ -233,28 +268,43 @@ function print_tree(printnode::Function, print_child_key::Function, io::IO, node
# Print children
s = Iterators.Stateful(this_printkeys ? pairs(c) : c)

while !isempty(s)
child_prefix = prefix
use_sibling_elision = !isnothing(maxsibling)
prev_child_value = nothing
seen_child = false
similar_run_length = 0
elided_count = 0

while !isempty(s)
if this_printkeys
child_key, child = popfirst!(s)
else
child = popfirst!(s)
child_key = nothing
end

print(io, prefix)
child_value = nodevalue(child)
if use_sibling_elision
if seen_child && nodevalue_distance(prev_child_value, child_value) <= sibling_similarity_threshold
similar_run_length += 1
else
if elided_count > 0
print_and_child_prefix(io, prefix, false, charset)
println(io, charset.trunc, " (", elided_count, " siblings elided)")
elided_count = 0
end
similar_run_length = 1
end
end

# Last child?
if isempty(s)
print(io, charset.terminator)
child_prefix *= " " ^ (textwidth(charset.skip) + textwidth(charset.dash) + 1)
else
print(io, charset.mid)
child_prefix *= charset.skip * " " ^ (textwidth(charset.dash) + 1)
prev_child_value = child_value
seen_child = true

if use_sibling_elision && similar_run_length > maxsibling
elided_count += 1
continue
end

print(io, charset.dash, ' ')
child_prefix = print_and_child_prefix(io, prefix, isempty(s) && elided_count == 0, charset)

# Print key
if this_printkeys
Expand All @@ -267,10 +317,17 @@ function print_tree(printnode::Function, print_child_key::Function, io::IO, node
end

print_tree(printnode, print_child_key, io, child;
maxdepth=maxdepth, indicate_truncation=indicate_truncation, charset=charset,
printkeys=printkeys, depth=depth+1, prefix=child_prefix, printnode_kw=printnode_kw,
maxdepth=maxdepth, maxsibling=maxsibling,
sibling_similarity_threshold=sibling_similarity_threshold,
indicate_truncation=indicate_truncation, charset=charset, printkeys=printkeys,
depth=depth+1, prefix=child_prefix, printnode_kw=printnode_kw,
)
end

if elided_count > 0
print_and_child_prefix(io, prefix, true, charset)
println(io, charset.trunc, " (", elided_count, " siblings elided)")
end
end

print_tree(printnode::Function, io::IO, node; kw...) = print_tree(printnode, print_child_key, io, node; kw...)
Expand Down
33 changes: 33 additions & 0 deletions test/printing.jl
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,39 @@ AbstractTrees.printnode(io::IO, u::UnindexableChildren) = AbstractTrees.printnod
@test numlines == 4 # 1 (head node) + 3 (depth)
end

@testset "Sibling elision" begin
truncation_str = TreeCharSet().trunc

# Start eliding only after the first N similar siblings.
ptxt = repr_tree([1, 1, 1, 2, 2, 2, 3], maxsibling=2)
@test endswith(ptxt, """
├─ 1
├─ 1
├─ $(truncation_str) (1 siblings elided)
├─ 2
├─ 2
├─ $(truncation_str) (1 siblings elided)
└─ 3
""")

# If an elided run ends the sibling list, the summary line is the terminator.
ptxt = repr_tree((1, 1, 1), maxsibling=2, printkeys=true)
@test endswith(ptxt, """
├─ 1 ⇒ 1
├─ 2 ⇒ 1
└─ $(truncation_str) (1 siblings elided)
""")

tree = UnindexableChildren([1, 1, 1, 2])
@test repr_tree(tree, maxsibling=2) == repr_tree(tree.node, maxsibling=2)

# Elided siblings are not recursed into, so they do not contribute maxdepth truncation lines.
ptxt = repr_tree([[1, 2], [1, 2], [1, 2], [3, 4]], maxdepth=1, maxsibling=2)
lines = [strip(line) for line in split(ptxt, '\n') if !isempty(strip(line))]
@test count(line -> endswith(line, truncation_str), lines) == 3
@test any(line -> occursin(" (1 siblings elided)", line), lines)
end


@testset "Child keys" begin
@testset "AbstractVector" begin
Expand Down
Loading