Skip to content

Commit 7ad8e1a

Browse files
committed
Better printing of the table and mtg
1 parent 9ffaabc commit 7ad8e1a

9 files changed

Lines changed: 103 additions & 16 deletions

File tree

Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6"
1111
MetaGraphsNext = "fa8bd995-216d-47f1-8a91-f3b68fbeb377"
1212
MutableNamedTuples = "af6c499f-54b4-48cc-bbd2-094bba7533c7"
1313
OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"
14+
PrettyTables = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d"
1415
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
1516
SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce"
1617
Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"
@@ -23,6 +24,7 @@ Graphs = "1"
2324
MetaGraphsNext = "0.5, 0.6, 0.7, 0.8"
2425
MutableNamedTuples = "0.1"
2526
OrderedCollections = "1.4"
27+
PrettyTables = "3"
2628
SHA = "0.7, 1"
2729
Tables = "1"
2830
XLSX = "0.7, 0.8, 0.9, 0.10"

docs/src/tutorials/4.convert_mtg.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ The unified table reports MTG topology columns plus all attributes:
2727
- parent_id: the node's parent id
2828
- link: the link between the node and its parent
2929

30+
`symbols` and `scales` are shown as table metadata in the display header (not as columns).
31+
3032
If you only need a few attributes, you can select them directly:
3133

3234
```@example usepkg

src/MultiScaleTreeGraph.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ using Printf
55
import MutableNamedTuples: MutableNamedTuple
66
using DelimitedFiles
77
using OrderedCollections
8+
import PrettyTables
89
import XLSX: readxlsx, sheetnames
910
import SHA: sha1 # for naming the cache variable
1011
import Base

src/conversion/Tables.jl

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ struct MTGAttrColumnView{N,T} <: AbstractVector{T}
33
key::Symbol
44
end
55

6+
const _TABLE_META_COLUMNS = (:description, :symbols, :scales)
7+
68
Base.IndexStyle(::Type{<:MTGAttrColumnView}) = IndexLinear()
79
Base.size(col::MTGAttrColumnView) = (length(col.nodes),)
810
Base.length(col::MTGAttrColumnView) = length(col.nodes)
@@ -61,6 +63,7 @@ function _collect_attr_names_from_store(store::MTGAttributeStore)
6163
for bucket in store.buckets
6264
for col in bucket.columns
6365
name = col.name
66+
name in _TABLE_META_COLUMNS && continue
6467
if !(name in seen)
6568
push!(seen, name)
6669
push!(out, name)
@@ -75,6 +78,7 @@ function _collect_attr_names_from_nodes(nodes)
7578
seen = Set{Symbol}()
7679
for n in nodes
7780
for key in attribute_names(n)
81+
key in _TABLE_META_COLUMNS && continue
7882
if !(key in seen)
7983
push!(seen, key)
8084
push!(out, key)
@@ -102,6 +106,24 @@ function _build_attr_columns(nodes, attr_names)
102106
cols
103107
end
104108

109+
@inline function _filter_meta_columns(vars)
110+
out = Symbol[]
111+
for v in vars
112+
v in _TABLE_META_COLUMNS || push!(out, v)
113+
end
114+
out
115+
end
116+
117+
function _table_metadata_from_mtg(mtg::Node)
118+
root = get_root(mtg)
119+
md = Dict{Symbol,Any}()
120+
syms = attribute(root, :symbols, default=nothing)
121+
scs = attribute(root, :scales, default=nothing)
122+
syms === nothing || (md[:symbols] = syms)
123+
scs === nothing || (md[:scales] = scs)
124+
return md
125+
end
126+
105127
@inline function _value_type_or_missing(values::Vector{Any})
106128
T = Union{}
107129
has_missing = false
@@ -128,6 +150,8 @@ If `vars` is provided (`Symbol`/`String`/vector/tuple), only these attributes ar
128150
function symbol_table(mtg::Node, symbol, vars=nothing)
129151
symbol_ = _to_table_key(symbol)
130152
vars_ = _to_table_vars(vars)
153+
vars_ === nothing || (vars_ = _filter_meta_columns(vars_))
154+
metadata = _table_metadata_from_mtg(mtg)
131155
attrs = node_attributes(get_root(mtg))
132156
if attrs isa ColumnarAttrs
133157
store = _store_for_node_attrs(attrs)
@@ -142,7 +166,7 @@ function symbol_table(mtg::Node, symbol, vars=nothing)
142166
push!(cols_, Missing[])
143167
end
144168
end
145-
return ColumnTable(names_, cols_)
169+
return ColumnTable(names_, cols_; metadata=metadata)
146170
end
147171
bucket = store.buckets[bid]
148172
names_ = Symbol[:node_id]
@@ -163,7 +187,7 @@ function symbol_table(mtg::Node, symbol, vars=nothing)
163187
end
164188
end
165189
end
166-
return ColumnTable(names_, cols_)
190+
return ColumnTable(names_, cols_; metadata=metadata)
167191
end
168192
end
169193

@@ -178,7 +202,7 @@ function symbol_table(mtg::Node, symbol, vars=nothing)
178202
push!(names_, attr_names[i])
179203
push!(cols_, attr_cols[i])
180204
end
181-
ColumnTable(names_, cols_)
205+
ColumnTable(names_, cols_; metadata=metadata)
182206
end
183207

184208
"""
@@ -191,6 +215,8 @@ If `vars` is provided (`Symbol`/`String`/vector/tuple), only these attributes ar
191215
function mtg_table(mtg::Node, vars=nothing)
192216
nodes = traverse(mtg, node -> node, type=typeof(mtg))
193217
vars_ = _to_table_vars(vars)
218+
vars_ === nothing || (vars_ = _filter_meta_columns(vars_))
219+
metadata = _table_metadata_from_mtg(mtg)
194220

195221
names_ = Symbol[:node_id, :symbol, :scale, :index, :link, :parent_id]
196222
cols_ = AbstractVector[
@@ -212,7 +238,7 @@ function mtg_table(mtg::Node, vars=nothing)
212238
T = _table_attr_type_from_store(store, key)
213239
push!(cols_, MTGAttrColumnView{typeof(nodes[1]),T}(nodes, key))
214240
end
215-
return ColumnTable(names_, cols_)
241+
return ColumnTable(names_, cols_; metadata=metadata)
216242
end
217243
end
218244

@@ -224,7 +250,7 @@ function mtg_table(mtg::Node, vars=nothing)
224250
push!(cols_, attr_cols[i])
225251
end
226252

227-
ColumnTable(names_, cols_)
253+
ColumnTable(names_, cols_; metadata=metadata)
228254
end
229255

230256
@inline function _materialize_table(source, sink)

src/print_MTG/print.jl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ mtg
2222
```
2323
"""
2424
function Base.print(node::Node; leading::AbstractString="", io::IO=stdout, limit=true)
25+
if isroot(node)
26+
syms = attribute(node, :symbols, default=nothing)
27+
scs = attribute(node, :scales, default=nothing)
28+
syms === nothing || print(io, "Symbols: ", syms, "\n")
29+
scs === nothing || print(io, "Scales: ", scs, "\n")
30+
end
31+
2532
node_vec = get_printing(node; leading=leading)
2633

2734
for (i, j) in enumerate(node_vec)

src/types/ColumnTable.jl

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ mutable struct ColumnTable
22
col_names::Vector{Symbol}
33
name_to_idx::Dict{Symbol,Int}
44
cols::Vector{AbstractVector}
5+
metadata::Dict{Symbol,Any}
56
end
67

7-
function ColumnTable(col_names::Vector{Symbol}, cols::Vector{AbstractVector})
8+
function ColumnTable(col_names::Vector{Symbol}, cols::Vector{AbstractVector}; metadata=Dict{Symbol,Any}())
89
length(col_names) == length(cols) || error("Column names and columns must have the same length.")
910
nrows = isempty(cols) ? 0 : length(cols[1])
1011
@inbounds for i in 2:length(cols)
@@ -14,17 +15,17 @@ function ColumnTable(col_names::Vector{Symbol}, cols::Vector{AbstractVector})
1415
@inbounds for i in eachindex(col_names)
1516
name_to_idx[col_names[i]] = i
1617
end
17-
ColumnTable(col_names, name_to_idx, cols)
18+
ColumnTable(col_names, name_to_idx, cols, Dict{Symbol,Any}(pairs(metadata)))
1819
end
1920

20-
function ColumnTable(; kwargs...)
21+
function ColumnTable(; metadata=Dict{Symbol,Any}(), kwargs...)
2122
names_ = Symbol[]
2223
cols_ = AbstractVector[]
2324
for (k, v) in pairs(kwargs)
2425
push!(names_, k)
2526
push!(cols_, v)
2627
end
27-
ColumnTable(names_, cols_)
28+
ColumnTable(names_, cols_; metadata=metadata)
2829
end
2930

3031
@inline function _column_idx(table::ColumnTable, name::Symbol)
@@ -39,14 +40,14 @@ Base.size(table::ColumnTable) = (isempty(table.cols) ? 0 : length(table.cols[1])
3940
Base.length(table::ColumnTable) = first(size(table))
4041

4142
function Base.getproperty(table::ColumnTable, name::Symbol)
42-
if name === :col_names || name === :name_to_idx || name === :cols
43+
if name === :col_names || name === :name_to_idx || name === :cols || name === :metadata
4344
return getfield(table, name)
4445
end
4546
return table.cols[_column_idx(table, name)]
4647
end
4748

4849
function Base.setproperty!(table::ColumnTable, name::Symbol, values)
49-
if name === :col_names || name === :name_to_idx || name === :cols
50+
if name === :col_names || name === :name_to_idx || name === :cols || name === :metadata
5051
setfield!(table, name, values)
5152
return values
5253
end
@@ -75,7 +76,7 @@ Base.getindex(table::ColumnTable, ::Colon, col::Int) = table.cols[col]
7576

7677
function Base.copy(table::ColumnTable)
7778
new_cols = AbstractVector[copy(col) for col in table.cols]
78-
ColumnTable(copy(table.col_names), new_cols)
79+
ColumnTable(copy(table.col_names), new_cols; metadata=copy(table.metadata))
7980
end
8081

8182
function Base.:(==)(a::ColumnTable, b::ColumnTable)
@@ -84,7 +85,8 @@ function Base.:(==)(a::ColumnTable, b::ColumnTable)
8485
@inbounds for i in eachindex(a.cols)
8586
a.cols[i] == b.cols[i] || return false
8687
end
87-
return true
88+
a.metadata == b.metadata || return false
89+
true
8890
end
8991

9092
function Base.isequal(a::ColumnTable, b::ColumnTable)
@@ -93,7 +95,8 @@ function Base.isequal(a::ColumnTable, b::ColumnTable)
9395
@inbounds for i in eachindex(a.cols)
9496
isequal(a.cols[i], b.cols[i]) || return false
9597
end
96-
return true
98+
isequal(a.metadata, b.metadata) || return false
99+
true
97100
end
98101

99102
function Base.sort!(table::ColumnTable, by::Symbol; kwargs...)
@@ -112,3 +115,33 @@ Tables.columnnames(table::ColumnTable) = Tuple(table.col_names)
112115
Tables.getcolumn(table::ColumnTable, i::Int) = table.cols[i]
113116
Tables.getcolumn(table::ColumnTable, name::Symbol) = table.cols[_column_idx(table, name)]
114117
Tables.schema(table::ColumnTable) = Tables.Schema(Tuple(table.col_names), Tuple(eltype.(table.cols)))
118+
119+
function Base.show(io::IO, ::MIME"text/plain", table::ColumnTable)
120+
nrows, ncols = size(table)
121+
title = "ColumnTable($(nrows) x $(ncols))"
122+
123+
syms = get(table.metadata, :symbols, nothing)
124+
scales = get(table.metadata, :scales, nothing)
125+
syms === nothing || print(io, "Symbols: ", syms, "\n")
126+
scales === nothing || print(io, "Scales: ", scales, "\n")
127+
128+
if ncols == 0
129+
print(io, title)
130+
return
131+
end
132+
133+
t_format = PrettyTables.TextTableFormat(
134+
borders=PrettyTables.text_table_borders__unicode_rounded
135+
)
136+
137+
PrettyTables.pretty_table(
138+
io,
139+
table;
140+
backend=:text,
141+
title=title,
142+
table_format=t_format,
143+
row_number_column_label="Row",
144+
row_labels=1:nrows,
145+
vertical_crop_mode=:middle,
146+
)
147+
end

test/test-columnar.jl

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ all_table = to_table(mtg)
4343
all_df = DataFrame(all_table)
4444
@test :node_id in Symbol.(names(all_df))
4545
@test :symbol in Symbol.(names(all_df))
46+
@test !(:symbols in Symbol.(names(all_df)))
47+
@test !(:scales in Symbol.(names(all_df)))
48+
@test !(:description in Symbol.(names(all_df)))
4649
@test all_df.node_id == list_nodes(mtg)
4750
@test any(ismissing, all_df.Width)
4851

@@ -55,6 +58,13 @@ all_selected_kw = to_table(mtg, vars=[:Width, :Length])
5558
all_df_sink = to_table(mtg, vars=[:Width, :Length], sink=DataFrame)
5659
@test :Width in Symbol.(names(all_df_sink))
5760

61+
all_table_str = sprint(show, MIME("text/plain"), all_selected_kw)
62+
@test occursin("ColumnTable", all_table_str)
63+
@test occursin("node_id", all_table_str)
64+
@test occursin("Width", all_table_str)
65+
@test occursin("Symbols:", all_table_str)
66+
@test occursin("Scales:", all_table_str)
67+
5868
# Hybrid descendants traversal strategy.
5969
@test descendants_strategy(mtg) == :auto
6070
descendants_strategy!(mtg, :indexed)

test/test-conversion.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ mtg = read_mtg("files/simple_plant.mtg")
22

33
@testset "Tables/DataFrame interoperability" begin
44
df_mtg = DataFrame(mtg)
5-
@test df_mtg[1, :scales] == [0, 1, 2, 3, 3]
5+
@test !(:scales in Symbol.(names(df_mtg)))
6+
@test !(:symbols in Symbol.(names(df_mtg)))
7+
@test !(:description in Symbol.(names(df_mtg)))
68
@test df_mtg[7, :Length] == 0.2
79
end
810

test/test-nodes.jl

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,11 @@ mtg = read_mtg(file)
9292
@testset "names" begin
9393
@test sort!(get_attributes(mtg)) == [:Length, :Width, :XEuler, :dateDeath, :description, :isAlive, :scales, :symbols]
9494
@test names(mtg) == get_attributes(mtg)
95-
@test sort(names(DataFrame(mtg))[7:end]) == sort(string.(names(mtg)))
95+
attrs_no_meta = setdiff(names(mtg), [:description, :symbols, :scales])
96+
@test sort(names(DataFrame(mtg))[7:end]) == sort(string.(attrs_no_meta))
97+
mtg_print = sprint(show, mtg)
98+
@test occursin("Symbols:", mtg_print)
99+
@test occursin("Scales:", mtg_print)
96100
end
97101

98102

0 commit comments

Comments
 (0)