Skip to content

Commit dd5281e

Browse files
committed
implement column selection for mtg_table / to_table
1 parent 64a80ec commit dd5281e

4 files changed

Lines changed: 80 additions & 13 deletions

File tree

docs/src/get_started.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,16 @@ Or by getting a tabular view of your MTG:
5656
mtg_table(mtg)
5757
```
5858

59-
Or directly transforming the MTG into a DataFrame:
59+
You can also select only a subset of attributes:
6060

6161
```@example usepkg
62+
mtg_table(mtg, [:Length, :Width])
63+
```
64+
65+
Or directly transforming the MTG into a DataFrame:
66+
67+
```julia
68+
using DataFrames
6269
DataFrame(mtg)
6370
```
6471

docs/src/tutorials/4.convert_mtg.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,24 @@ 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+
If you only need a few attributes, you can select them directly:
31+
32+
```@example usepkg
33+
mtg_table(mtg, [:Width, :Length])
34+
```
35+
3036
It is also possible to get a per-symbol table:
3137

3238
```@example usepkg
3339
leaf_tbl = symbol_table(mtg, :Leaf)
3440
```
3541

42+
Per-symbol tables can also select attributes:
43+
44+
```@example usepkg
45+
symbol_table(mtg, :Leaf, [:Width, :Length])
46+
```
47+
3648
If you use `DataFrames.jl` in your own project, `DataFrame(mtg)` still works because MTGs implement the `Tables.jl` interface.
3749

3850
```julia

src/conversion/Tables.jl

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,17 @@ end
1515
@inline _to_table_key(key::Symbol) = key
1616
@inline _to_table_key(key) = Symbol(key)
1717

18+
@inline _to_table_vars(vars::Nothing) = nothing
19+
@inline _to_table_vars(var::Symbol) = Symbol[var]
20+
@inline _to_table_vars(var::AbstractString) = Symbol[Symbol(var)]
21+
@inline function _to_table_vars(vars::Union{Tuple,AbstractVector})
22+
out = Vector{Symbol}(undef, length(vars))
23+
@inbounds for i in eachindex(vars)
24+
out[i] = _to_table_key(vars[i])
25+
end
26+
out
27+
end
28+
1829
@inline function _table_attr_type_from_store(store::MTGAttributeStore, key::Symbol)
1930
has_any = false
2031
has_missing = false
@@ -109,33 +120,56 @@ end
109120
end
110121

111122
"""
112-
symbol_table(mtg::Node, symbol)
123+
symbol_table(mtg::Node, symbol, vars=nothing)
113124
114125
Return a per-symbol column table view (Tables.jl-compatible).
126+
If `vars` is provided (`Symbol`/`String`/vector/tuple), only these attributes are included.
115127
"""
116-
function symbol_table(mtg::Node, symbol)
128+
function symbol_table(mtg::Node, symbol, vars=nothing)
117129
symbol_ = _to_table_key(symbol)
130+
vars_ = _to_table_vars(vars)
118131
attrs = node_attributes(get_root(mtg))
119132
if attrs isa ColumnarAttrs
120133
store = _store_for_node_attrs(attrs)
121134
if store !== nothing
122135
bid = get(store.symbol_to_bucket, symbol_, 0)
123136
if bid == 0
124-
return ColumnTable(Symbol[:node_id], AbstractVector[Int[]])
137+
names_ = Symbol[:node_id]
138+
cols_ = AbstractVector[Int[]]
139+
if vars_ !== nothing
140+
for key in vars_
141+
push!(names_, key)
142+
push!(cols_, Missing[])
143+
end
144+
end
145+
return ColumnTable(names_, cols_)
125146
end
126147
bucket = store.buckets[bid]
127148
names_ = Symbol[:node_id]
128149
cols_ = AbstractVector[bucket.row_to_node]
129-
for col in bucket.columns
130-
push!(names_, col.name)
131-
push!(cols_, col.data)
150+
if vars_ === nothing
151+
for col in bucket.columns
152+
push!(names_, col.name)
153+
push!(cols_, col.data)
154+
end
155+
else
156+
for key in vars_
157+
col_idx = get(bucket.col_index, key, 0)
158+
push!(names_, key)
159+
if col_idx == 0
160+
push!(cols_, fill(missing, length(bucket.row_to_node)))
161+
else
162+
push!(cols_, bucket.columns[col_idx].data)
163+
end
164+
end
132165
end
133166
return ColumnTable(names_, cols_)
134167
end
135168
end
136169

137170
nodes = traverse(mtg, node -> node, symbol=symbol_, type=typeof(mtg))
138171
attr_names = _collect_attr_names_from_nodes(nodes)
172+
attr_names = vars_ === nothing ? attr_names : vars_
139173
attr_cols = _build_attr_columns(nodes, attr_names)
140174

141175
names_ = Symbol[:node_id]
@@ -148,13 +182,15 @@ function symbol_table(mtg::Node, symbol)
148182
end
149183

150184
"""
151-
mtg_table(mtg::Node)
185+
mtg_table(mtg::Node, vars=nothing)
152186
153187
Return a unified traversal-ordered table view of an MTG (Tables.jl-compatible).
154188
Absent attributes are represented as `missing`.
189+
If `vars` is provided (`Symbol`/`String`/vector/tuple), only these attributes are included.
155190
"""
156-
function mtg_table(mtg::Node)
191+
function mtg_table(mtg::Node, vars=nothing)
157192
nodes = traverse(mtg, node -> node, type=typeof(mtg))
193+
vars_ = _to_table_vars(vars)
158194

159195
names_ = Symbol[:node_id, :symbol, :scale, :index, :link, :parent_id]
160196
cols_ = AbstractVector[
@@ -170,7 +206,7 @@ function mtg_table(mtg::Node)
170206
if attrs isa ColumnarAttrs
171207
store = _store_for_node_attrs(attrs)
172208
if store !== nothing
173-
attr_names = _collect_attr_names_from_store(store)
209+
attr_names = vars_ === nothing ? _collect_attr_names_from_store(store) : vars_
174210
for key in attr_names
175211
push!(names_, key)
176212
T = _table_attr_type_from_store(store, key)
@@ -181,6 +217,7 @@ function mtg_table(mtg::Node)
181217
end
182218

183219
attr_names = _collect_attr_names_from_nodes(nodes)
220+
attr_names = vars_ === nothing ? attr_names : vars_
184221
attr_cols = _build_attr_columns(nodes, attr_names)
185222
@inbounds for i in eachindex(attr_names)
186223
push!(names_, attr_names[i])
@@ -191,14 +228,15 @@ function mtg_table(mtg::Node)
191228
end
192229

193230
"""
194-
to_table(mtg::Node; symbol=nothing)
231+
to_table(mtg::Node; symbol=nothing, vars=nothing)
195232
196233
Convenience helper to get a Tables.jl-compatible view from an MTG.
197234
- `symbol=nothing`: unified traversal-ordered table (`mtg_table`)
198235
- `symbol=<symbol>`: per-symbol table (`symbol_table`)
236+
- `vars`: optional attribute selection (`Symbol`/`String`/vector/tuple)
199237
"""
200-
function to_table(mtg::Node; symbol=nothing)
201-
symbol === nothing ? mtg_table(mtg) : symbol_table(mtg, symbol)
238+
function to_table(mtg::Node; symbol=nothing, vars=nothing)
239+
symbol === nothing ? mtg_table(mtg, vars) : symbol_table(mtg, symbol, vars)
202240
end
203241

204242
Tables.istable(::Type{<:Node}) = true

test/test-columnar.jl

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,23 @@ leaf_df = DataFrame(leaf_table)
3535
@test :Width in Symbol.(names(leaf_df))
3636
@test nrow(leaf_df) > 0
3737

38+
leaf_selected = symbol_table(mtg, :Leaf, [:Width, "Length"])
39+
@test Tables.columnnames(leaf_selected) == (:node_id, :Width, :Length)
40+
@test length(Tables.getcolumn(leaf_selected, :Width)) == nrow(leaf_df)
41+
3842
all_table = mtg_table(mtg)
3943
all_df = DataFrame(all_table)
4044
@test :node_id in Symbol.(names(all_df))
4145
@test :symbol in Symbol.(names(all_df))
4246
@test all_df.node_id == list_nodes(mtg)
4347
@test any(ismissing, all_df.Width)
4448

49+
all_selected = mtg_table(mtg, [:Width, "Length"])
50+
@test Tables.columnnames(all_selected) == (:node_id, :symbol, :scale, :index, :link, :parent_id, :Width, :Length)
51+
52+
all_selected_kw = to_table(mtg, vars=[:Width, :Length])
53+
@test Tables.columnnames(all_selected_kw) == Tables.columnnames(all_selected)
54+
4555
# Hybrid descendants traversal strategy.
4656
@test descendants_strategy(mtg) == :auto
4757
descendants_strategy!(mtg, :indexed)

0 commit comments

Comments
 (0)