|
1 | 1 | module TableView |
2 | 2 |
|
3 | | -using WebIO |
4 | | -using JSExpr |
5 | | -using JuliaDB |
6 | | -using DataValues |
| 3 | +using Tables |
| 4 | +using WebIO, JSExpr, JSON, Dates, UUIDs |
| 5 | +using Observables: @map |
7 | 6 |
|
8 | | -import JuliaDB: DNDSparse, DNextTable, NextTable |
| 7 | +export showtable |
9 | 8 |
|
10 | | -function JuliaDB.subtable(t::DNextTable, r) |
11 | | - table(collect(rows(t)[r]), pkey=t.pkey) |
12 | | -end |
| 9 | +const ag_grid_imports = [] |
13 | 10 |
|
14 | | -showna(xs) = xs |
15 | | -function showna(xs::AbstractArray{T}) where {T<:DataValue} |
16 | | - map(xs) do x |
17 | | - isnull(x) ? "NA" : get(x) |
| 11 | +function __init__() |
| 12 | + empty!(ag_grid_imports) |
| 13 | + for f in ["ag-grid.js", "ag-grid.css", "ag-grid-light.css", "ag-grid-dark.css"] |
| 14 | + push!(ag_grid_imports, normpath(joinpath(@__DIR__, "..", "deps", "ag-grid", f))) |
18 | 15 | end |
19 | 16 | end |
20 | 17 |
|
21 | | -function showna(xs::Columns) |
22 | | - rows(map(showna, columns(xs))) |
23 | | -end |
24 | | - |
25 | | -function showtable(t::Union{DNextTable, NextTable}; rows=1:100, colopts=Dict(), kwargs...) |
26 | | - w = Scope(imports=["https://cdnjs.cloudflare.com/ajax/libs/handsontable/0.34.0/handsontable.full.js", |
27 | | - "https://cdnjs.cloudflare.com/ajax/libs/handsontable/0.34.0/handsontable.full.css"]) |
| 18 | +function showtable(table; dark = false, height = 500) |
| 19 | + if !Tables.istable(typeof(table)) |
| 20 | + throw(ArgumentError("Argument is not a table.")) |
| 21 | + end |
28 | 22 |
|
29 | | - trunc_rows = max(1, first(rows)):min(length(t), last(rows)) |
30 | | - subt = JuliaDB.subtable(t, trunc_rows) |
| 23 | + tablelength = Base.IteratorSize(table) == Base.HasLength() ? length(Tables.rows(table)) : nothing |
| 24 | + |
| 25 | + rows = Tables.rows(table) |
| 26 | + schema = Tables.schema(table) |
| 27 | + if schema === nothing |
| 28 | + types = [] |
| 29 | + for (i, c) in enumerate(Tables.eachcolumn(first(rows))) |
| 30 | + push!(types, typeof(c)) |
| 31 | + end |
| 32 | + names = collect(propertynames(first(rows))) |
| 33 | + else |
| 34 | + names = schema.names |
| 35 | + types = schema.types |
| 36 | + end |
| 37 | + w = Scope(imports = ag_grid_imports) |
| 38 | + |
| 39 | + coldefs = [( |
| 40 | + headerName = n, |
| 41 | + headerTooltip = types[i], |
| 42 | + field = n, |
| 43 | + type = types[i] <: Union{Missing, T where T <: Number} ? "numericColumn" : nothing, |
| 44 | + filter = types[i] <: Union{Missing, T where T <: Dates.Date} ? "agDateColumnFilter" : |
| 45 | + types[i] <: Union{Missing, T where T <: Number} ? "agNumberColumnFilter" : nothing |
| 46 | + ) for (i, n) in enumerate(names)] |
| 47 | + |
| 48 | + id = string("grid-", string(uuid1())[1:8]) |
| 49 | + w.dom = dom"div"(className = "ag-theme-balham$(dark ? "-dark" : "")", |
| 50 | + style = Dict("width" => "100%", |
| 51 | + "height" => "$(height)px"), |
| 52 | + id = id) |
| 53 | + |
| 54 | + tablelength === nothing || tablelength > 10_000 ? _showtable_async!(w, names, types, rows, coldefs, tablelength, dark, id) : |
| 55 | + _showtable_sync!(w, names, types, rows, coldefs, tablelength, dark, id) |
31 | 56 |
|
32 | | - headers = colnames(subt) |
33 | | - cols = [merge(Dict(:data=>n), get(colopts, n, Dict())) for n in headers] |
| 57 | + w |
| 58 | +end |
34 | 59 |
|
| 60 | +function _showtable_sync!(w, names, types, rows, coldefs, tablelength, dark, id) |
35 | 61 | options = Dict( |
36 | | - :data => showna(collect(JuliaDB.rows(subt))), |
37 | | - :colHeaders => headers, |
38 | | - :modifyColWidth => @js(w -> w > 300 ? 300 : w), |
39 | | - :modifyRowHeight => @js(h -> h > 60 ? 50 : h), |
40 | | - :manualColumnResize => true, |
41 | | - :manualRowResize => true, |
42 | | - :columns => cols, |
43 | | - :width => 800, |
44 | | - :height => 400, |
| 62 | + :rowData => JSONText(table2json(rows, names, types)), |
| 63 | + :columnDefs => coldefs, |
| 64 | + :enableSorting => true, |
| 65 | + :enableFilter => true, |
| 66 | + :enableColResize => true, |
| 67 | + :multiSortKey => "ctrl", |
45 | 68 | ) |
46 | | - if (length(t.pkey) > 0 && t.pkey == [1:length(t.pkey);]) |
47 | | - options[:fixedColumnsLeft] = length(t.pkey) |
48 | | - end |
49 | 69 |
|
50 | | - merge!(options, Dict(kwargs)) |
51 | | - |
52 | | - handler = @js function (Handsontable) |
53 | | - @var sizefix = document.createElement("style"); |
54 | | - sizefix.textContent = """ |
55 | | - .htCore td { |
56 | | - white-space:nowrap |
57 | | - } |
58 | | - """ |
59 | | - this.dom.appendChild(sizefix) |
60 | | - this.hot = @new Handsontable(this.dom, $options); |
| 70 | + handler = @js function (agGrid) |
| 71 | + @var gridOptions = $options |
| 72 | + @var el = document.getElementById($id) |
| 73 | + this.table = @new agGrid.Grid(el, gridOptions) |
| 74 | + gridOptions.columnApi.autoSizeColumns($names) |
61 | 75 | end |
62 | 76 | onimport(w, handler) |
63 | | - w.dom = dom"div"() |
64 | | - w |
65 | 77 | end |
66 | 78 |
|
67 | | -function showtable(t::Union{DNDSparse, NDSparse}; rows=1:100, colopts=Dict(), kwargs...) |
68 | | - w = Scope(imports=["https://cdnjs.cloudflare.com/ajax/libs/handsontable/0.34.0/handsontable.full.js", |
69 | | - "https://cdnjs.cloudflare.com/ajax/libs/handsontable/0.34.0/handsontable.full.css"]) |
70 | | - data = Observable{Any}(w, "data", []) |
71 | | - |
72 | | - trunc_rows = max(1, first(rows)):min(length(t), last(rows)) |
73 | 79 |
|
74 | | - ks = keys(t)[trunc_rows] |
75 | | - vs = values(t)[trunc_rows] |
76 | | - |
77 | | - if !isa(keys(t), Columns) |
78 | | - ks = collect(ks) |
79 | | - vs = collect(vs) |
| 80 | +function _showtable_async!(w, names, types, rows, coldefs, tablelength, dark, id) |
| 81 | + rowparams = Observable(w, "rowparams", Dict("startRow" => 1, |
| 82 | + "endRow" => 100, |
| 83 | + "successCallback" => @js v -> nothing)) |
| 84 | + requestedrows = Observable(w, "requestedrows", JSONText("{}")) |
| 85 | + on(rowparams) do x |
| 86 | + requestedrows[] = JSONText(table2json(rows, names, types, requested = [x["startRow"], x["endRow"]])) |
80 | 87 | end |
81 | 88 |
|
82 | | - subt = NDSparse(showna(ks), showna(vs)) |
83 | | - |
84 | | - headers = colnames(subt) |
85 | | - cols = [merge(Dict(:data=>n), get(colopts, n, Dict())) for n in headers] |
| 89 | + onjs(requestedrows, @js function (val) |
| 90 | + ($rowparams[]).successCallback(val, $(tablelength)) |
| 91 | + end) |
86 | 92 |
|
87 | 93 | options = Dict( |
88 | | - :data => JuliaDB.rows(subt), |
89 | | - :colHeaders => headers, |
90 | | - :fixedColumnsLeft => ndims(t), |
91 | | - :modifyColWidth => @js(w -> w > 300 ? 300 : w), |
92 | | - :modifyRowHeight => @js(h -> h > 60 ? 50 : h), |
93 | | - :manualColumnResize => true, |
94 | | - :manualRowResize => true, |
95 | | - :columns => cols, |
96 | | - :width => 800, |
97 | | - :height => 400, |
| 94 | + :columnDefs => coldefs, |
| 95 | + :enableSorting => true, |
| 96 | + :enableFilter => true, |
| 97 | + :maxConcurrentDatasourceRequests => 1, |
| 98 | + :cacheBlockSize => 1000, |
| 99 | + :maxBlocksInCache => 100, |
| 100 | + :enableColResize => true, |
| 101 | + :multiSortKey => "ctrl", |
| 102 | + :rowModelType => "infinite", |
| 103 | + :datasource => Dict( |
| 104 | + "getRows" => |
| 105 | + @js function (rowParams) |
| 106 | + $rowparams[] = rowParams |
| 107 | + end |
| 108 | + , |
| 109 | + "rowCount" => tablelength |
| 110 | + ) |
98 | 111 | ) |
99 | 112 |
|
100 | | - merge!(options, Dict(kwargs)) |
101 | | - |
102 | | - handler = @js function (Handsontable) |
103 | | - @var sizefix = document.createElement("style"); |
104 | | - sizefix.textContent = """ |
105 | | - .htCore td { |
106 | | - white-space:nowrap |
107 | | - } |
108 | | - """ |
109 | | - this.dom.appendChild(sizefix) |
110 | | - this.hot = @new Handsontable(this.dom, $options); |
| 113 | + handler = @js function (agGrid) |
| 114 | + @var gridOptions = $options |
| 115 | + @var el = document.getElementById($id) |
| 116 | + this.table = @new agGrid.Grid(el, gridOptions) |
| 117 | + gridOptions.columnApi.autoSizeColumns($names) |
111 | 118 | end |
112 | 119 | onimport(w, handler) |
113 | | - w.dom = dom"div"() |
114 | | - w |
115 | 120 | end |
116 | 121 |
|
117 | | -showtable(t; kwargs...) = showtable(table(t); kwargs...) |
| 122 | +# directly write JSON instead of allocating temporary dicts etc |
| 123 | +function table2json(rows, names, types; requested = nothing) |
| 124 | + io = IOBuffer() |
| 125 | + print(io, '[') |
| 126 | + for (i, row) in enumerate(rows) |
| 127 | + if requested == nothing || first(requested) <= i <= last(requested) |
| 128 | + print(io, '{') |
| 129 | + i = 1 |
| 130 | + for col in Tables.eachcolumn(row) |
| 131 | + JSON.print(io, names[i]) |
| 132 | + i += 1 |
| 133 | + print(io, ':') |
| 134 | + if col isa Number |
| 135 | + JSON.print(io, col) |
| 136 | + else |
| 137 | + JSON.print(io, sprint(print, col)) |
| 138 | + end |
| 139 | + print(io, ',') |
| 140 | + end |
| 141 | + skip(io, -1) |
| 142 | + print(io, '}') |
| 143 | + print(io, ',') |
| 144 | + end |
| 145 | + end |
| 146 | + skip(io, -1) |
| 147 | + print(io, ']') |
118 | 148 |
|
119 | | -end # module |
| 149 | + String(take!(io)) |
| 150 | +end |
| 151 | +end |
0 commit comments