visidata 3.0.1__py3-none-any.whl → 3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- visidata/__init__.py +12 -10
- visidata/_input.py +208 -199
- visidata/_open.py +4 -1
- visidata/_types.py +4 -3
- visidata/aggregators.py +88 -39
- visidata/apps/vdsql/_ibis.py +9 -11
- visidata/apps/vdsql/clickhouse.py +2 -2
- visidata/apps/vdsql/snowflake.py +1 -1
- visidata/apps/vgit/status.py +1 -1
- visidata/basesheet.py +11 -4
- visidata/canvas.py +66 -24
- visidata/clipboard.py +13 -6
- visidata/cliptext.py +7 -6
- visidata/cmdlog.py +40 -27
- visidata/column.py +14 -49
- visidata/ddw/regex.ddw +3 -2
- visidata/deprecated.py +14 -2
- visidata/desktop/visidata.desktop +2 -2
- visidata/editor.py +1 -0
- visidata/errors.py +1 -1
- visidata/experimental/sort_selected.py +54 -0
- visidata/expr.py +69 -18
- visidata/features/change_precision.py +1 -3
- visidata/features/cmdpalette.py +23 -4
- visidata/features/colorsheet.py +1 -1
- visidata/features/dedupe.py +3 -3
- visidata/features/go_col.py +71 -0
- visidata/features/graph_seaborn.py +1 -1
- visidata/features/join.py +20 -10
- visidata/features/layout.py +18 -5
- visidata/features/ping.py +16 -12
- visidata/features/regex.py +5 -5
- visidata/features/slide.py +15 -17
- visidata/features/status_source.py +3 -1
- visidata/features/sysedit.py +1 -1
- visidata/features/transpose.py +2 -1
- visidata/features/type_ipaddr.py +2 -4
- visidata/features/unfurl.py +1 -0
- visidata/form.py +2 -2
- visidata/freqtbl.py +16 -11
- visidata/fuzzymatch.py +1 -0
- visidata/graph.py +173 -12
- visidata/guide.py +61 -25
- visidata/guides/ClipboardGuide.md +48 -0
- visidata/guides/ColumnsGuide.md +52 -0
- visidata/guides/CommandsSheet.md +28 -0
- visidata/guides/DirSheet.md +34 -0
- visidata/guides/ErrorsSheet.md +17 -0
- visidata/guides/FrequencyTable.md +42 -0
- visidata/guides/GrepSheet.md +28 -0
- visidata/guides/JsonSheet.md +38 -0
- visidata/guides/MacrosSheet.md +19 -0
- visidata/guides/MeltGuide.md +52 -0
- visidata/guides/MemorySheet.md +7 -0
- visidata/guides/MenuGuide.md +26 -0
- visidata/guides/ModifyGuide.md +38 -0
- visidata/guides/PivotGuide.md +71 -0
- visidata/guides/RegexGuide.md +107 -0
- visidata/guides/SelectionGuide.md +44 -0
- visidata/guides/SlideGuide.md +26 -0
- visidata/guides/SortGuide.md +0 -0
- visidata/guides/SplitpaneGuide.md +15 -0
- visidata/guides/TypesSheet.md +43 -0
- visidata/guides/XsvGuide.md +36 -0
- visidata/help.py +6 -6
- visidata/hint.py +2 -1
- visidata/indexsheet.py +2 -2
- visidata/interface.py +13 -14
- visidata/keys.py +4 -1
- visidata/loaders/api_airtable.py +1 -1
- visidata/loaders/archive.py +1 -1
- visidata/loaders/csv.py +9 -5
- visidata/loaders/eml.py +11 -6
- visidata/loaders/f5log.py +1 -0
- visidata/loaders/fec.py +18 -42
- visidata/loaders/fixed_width.py +19 -3
- visidata/loaders/grep.py +121 -0
- visidata/loaders/html.py +1 -0
- visidata/loaders/http.py +6 -1
- visidata/loaders/json.py +22 -4
- visidata/loaders/jsonla.py +8 -2
- visidata/loaders/mailbox.py +1 -0
- visidata/loaders/markdown.py +25 -6
- visidata/loaders/msgpack.py +19 -0
- visidata/loaders/npy.py +0 -1
- visidata/loaders/odf.py +18 -4
- visidata/loaders/orgmode.py +1 -1
- visidata/loaders/rec.py +6 -4
- visidata/loaders/sas.py +11 -4
- visidata/loaders/scrape.py +0 -1
- visidata/loaders/texttables.py +2 -0
- visidata/loaders/tsv.py +24 -7
- visidata/loaders/unzip_http.py +127 -3
- visidata/loaders/vds.py +4 -0
- visidata/loaders/vdx.py +1 -1
- visidata/loaders/xlsx.py +5 -0
- visidata/loaders/xml.py +2 -1
- visidata/macros.py +14 -31
- visidata/main.py +20 -15
- visidata/mainloop.py +17 -6
- visidata/man/vd.1 +74 -39
- visidata/man/vd.txt +73 -41
- visidata/memory.py +16 -5
- visidata/menu.py +14 -3
- visidata/metasheets.py +5 -6
- visidata/modify.py +4 -4
- visidata/mouse.py +2 -0
- visidata/movement.py +14 -28
- visidata/optionssheet.py +3 -5
- visidata/path.py +59 -37
- visidata/pivot.py +8 -5
- visidata/pyobj.py +63 -9
- visidata/rename_col.py +18 -1
- visidata/save.py +16 -9
- visidata/search.py +4 -4
- visidata/selection.py +10 -56
- visidata/settings.py +37 -35
- visidata/sheets.py +189 -118
- visidata/shell.py +23 -14
- visidata/sidebar.py +71 -16
- visidata/sort.py +21 -6
- visidata/statusbar.py +42 -5
- visidata/stored_list.py +5 -2
- visidata/tests/conftest.py +1 -0
- visidata/tests/test_commands.py +9 -1
- visidata/tests/test_completer.py +18 -0
- visidata/tests/test_edittext.py +3 -2
- visidata/text_source.py +7 -4
- visidata/textsheet.py +20 -6
- visidata/themes/ascii8.py +9 -6
- visidata/themes/asciimono.py +14 -4
- visidata/threads.py +13 -3
- visidata/tuiwin.py +5 -1
- visidata/type_currency.py +1 -2
- visidata/type_date.py +6 -1
- visidata/undo.py +10 -13
- visidata/utils.py +9 -3
- visidata/vdobj.py +21 -1
- visidata/wrappers.py +9 -1
- {visidata-3.0.1.data → visidata-3.1.data}/data/share/applications/visidata.desktop +2 -2
- {visidata-3.0.1.data → visidata-3.1.data}/data/share/man/man1/vd.1 +74 -39
- {visidata-3.0.1.data → visidata-3.1.data}/data/share/man/man1/visidata.1 +74 -39
- {visidata-3.0.1.dist-info → visidata-3.1.dist-info}/METADATA +33 -5
- visidata-3.1.dist-info/RECORD +284 -0
- visidata-3.0.1.dist-info/RECORD +0 -258
- {visidata-3.0.1.data → visidata-3.1.data}/scripts/vd +0 -0
- {visidata-3.0.1.data → visidata-3.1.data}/scripts/vd2to3.vdx +0 -0
- {visidata-3.0.1.dist-info → visidata-3.1.dist-info}/LICENSE.gpl3 +0 -0
- {visidata-3.0.1.dist-info → visidata-3.1.dist-info}/WHEEL +0 -0
- {visidata-3.0.1.dist-info → visidata-3.1.dist-info}/entry_points.txt +0 -0
- {visidata-3.0.1.dist-info → visidata-3.1.dist-info}/top_level.txt +0 -0
visidata/_types.py
CHANGED
@@ -53,9 +53,8 @@ def numericFormatter(vd, fmtstr, typedval):
|
|
53
53
|
def numericType(vd, icon='', fmtstr='', formatter=vd.numericFormatter):
|
54
54
|
'''Decorator for numeric types.'''
|
55
55
|
def _decorator(f):
|
56
|
-
vd.addType(f, icon=icon, fmtstr=fmtstr, formatter=formatter)
|
56
|
+
vd.addType(f, icon=icon, fmtstr=fmtstr, formatter=formatter, name=f.__name__)
|
57
57
|
vd.numericTypes.append(f)
|
58
|
-
vd.addGlobals({f.__name__: f})
|
59
58
|
return f
|
60
59
|
return _decorator
|
61
60
|
|
@@ -82,6 +81,8 @@ def addType(vd, typetype=None, icon=None, fmtstr='', formatter=vd.numericFormatt
|
|
82
81
|
t = VisiDataType(typetype=typetype, icon=icon, fmtstr=fmtstr, formatter=formatter, key=key, name=name)
|
83
82
|
if typetype:
|
84
83
|
vd.typemap[typetype] = t
|
84
|
+
if name:
|
85
|
+
vd.addGlobals({name: typetype})
|
85
86
|
return t
|
86
87
|
|
87
88
|
vdtype = vd.addType
|
@@ -94,7 +95,7 @@ vd.typemap = {}
|
|
94
95
|
def getType(vd, typetype):
|
95
96
|
return vd.typemap.get(typetype) or VisiDataType()
|
96
97
|
|
97
|
-
vdtype(None, '∅')
|
98
|
+
vdtype(None, '∅', name='none')
|
98
99
|
vdtype(anytype, '', formatter=lambda _,v: str(v))
|
99
100
|
vdtype(str, '~', formatter=lambda _,v: v)
|
100
101
|
vdtype(int, '#')
|
visidata/aggregators.py
CHANGED
@@ -5,7 +5,7 @@ import collections
|
|
5
5
|
import statistics
|
6
6
|
|
7
7
|
from visidata import Progress, Sheet, Column, ColumnsSheet, VisiData
|
8
|
-
from visidata import vd, anytype, vlen, asyncthread, wrapply, AttrDict
|
8
|
+
from visidata import vd, anytype, vlen, asyncthread, wrapply, AttrDict, date, INPROGRESS
|
9
9
|
|
10
10
|
vd.help_aggregators = '''# Choose Aggregators
|
11
11
|
Start typing an aggregator name or description.
|
@@ -48,10 +48,15 @@ def getValues(self, rows):
|
|
48
48
|
vd.aggregators = collections.OrderedDict() # [aggname] -> annotated func, or list of same
|
49
49
|
|
50
50
|
Column.init('aggstr', str, copy=True)
|
51
|
+
Column.init('_aggregatedTotals', dict) # [aggname] -> agg total over all rows
|
51
52
|
|
52
53
|
def aggregators_get(col):
|
53
54
|
'A space-separated names of aggregators on this column.'
|
54
|
-
|
55
|
+
aggs = []
|
56
|
+
for k in (col.aggstr or '').split():
|
57
|
+
agg = vd.aggregators[k]
|
58
|
+
aggs += agg if isinstance(agg, list) else [agg]
|
59
|
+
return aggs
|
55
60
|
|
56
61
|
def aggregators_set(col, aggs):
|
57
62
|
if isinstance(aggs, str):
|
@@ -71,32 +76,28 @@ Column.aggregators = property(aggregators_get, aggregators_set)
|
|
71
76
|
|
72
77
|
|
73
78
|
class Aggregator:
|
74
|
-
def __init__(self, name, type,
|
75
|
-
'Define aggregator `name` that calls
|
79
|
+
def __init__(self, name, type, funcValues=None, helpstr='foo'):
|
80
|
+
'Define aggregator `name` that calls funcValues(values)'
|
76
81
|
self.type = type
|
77
|
-
self.
|
78
|
-
self.funcValues = funcValues # funcValues(values, *args)
|
82
|
+
self.funcValues = funcValues # funcValues(values)
|
79
83
|
self.helpstr = helpstr
|
80
84
|
self.name = name
|
81
85
|
|
82
|
-
def
|
83
|
-
return self.func(*args, **kwargs)
|
84
|
-
|
85
|
-
_defaggr = Aggregator
|
86
|
-
|
87
|
-
@VisiData.api
|
88
|
-
def aggregator(vd, name, funcValues, helpstr='', *args, type=None):
|
89
|
-
'Define simple aggregator *name* that calls ``funcValues(values, *args)`` to aggregate *values*. Use *type* to force the default type of the aggregated column.'
|
90
|
-
def _funcRows(col, rows): # wrap builtins so they can have a .type
|
86
|
+
def aggregate(self, col, rows): # wrap builtins so they can have a .type
|
91
87
|
vals = list(col.getValues(rows))
|
92
88
|
try:
|
93
|
-
return funcValues(vals
|
89
|
+
return self.funcValues(vals)
|
94
90
|
except Exception as e:
|
95
91
|
if len(vals) == 0:
|
96
92
|
return None
|
97
|
-
|
93
|
+
raise e
|
98
94
|
|
99
|
-
|
95
|
+
|
96
|
+
@VisiData.api
|
97
|
+
def aggregator(vd, name, funcValues, helpstr='', *, type=None):
|
98
|
+
'''Define simple aggregator *name* that calls ``funcValues(values)`` to aggregate *values*.
|
99
|
+
Use *type* to force type of aggregated column (default to use type of source column).'''
|
100
|
+
vd.aggregators[name] = Aggregator(name, type, funcValues=funcValues, helpstr=helpstr)
|
100
101
|
|
101
102
|
## specific aggregator implementations
|
102
103
|
|
@@ -105,12 +106,9 @@ def mean(vals):
|
|
105
106
|
if vals:
|
106
107
|
return float(sum(vals))/len(vals)
|
107
108
|
|
108
|
-
def
|
109
|
+
def vsum(vals):
|
109
110
|
return sum(vals, start=type(vals[0] if len(vals) else 0)()) #1996
|
110
111
|
|
111
|
-
# start parameter in sum() added in Python 3.8
|
112
|
-
vsum = _vsum if sys.version_info[:2] >= (3, 8) else sum
|
113
|
-
|
114
112
|
# http://code.activestate.com/recipes/511478-finding-the-percentile-of-the-values/
|
115
113
|
def _percentile(N, percent, key=lambda x:x):
|
116
114
|
"""
|
@@ -134,11 +132,18 @@ def _percentile(N, percent, key=lambda x:x):
|
|
134
132
|
return d0+d1
|
135
133
|
|
136
134
|
@functools.lru_cache(100)
|
137
|
-
|
138
|
-
|
135
|
+
class PercentileAggregator(Aggregator):
|
136
|
+
def __init__(self, pct, helpstr=''):
|
137
|
+
super().__init__('p%s'%pct, None, helpstr=helpstr)
|
138
|
+
self.pct = pct
|
139
|
+
|
140
|
+
def aggregate(self, col, rows):
|
141
|
+
return _percentile(sorted(col.getValues(rows)), self.pct/100, key=float)
|
142
|
+
|
139
143
|
|
140
144
|
def quantiles(q, helpstr):
|
141
|
-
return [
|
145
|
+
return [PercentileAggregator(round(100*i/q), helpstr) for i in range(1, q)]
|
146
|
+
|
142
147
|
|
143
148
|
vd.aggregator('min', min, 'minimum value')
|
144
149
|
vd.aggregator('max', max, 'maximum value')
|
@@ -149,7 +154,7 @@ vd.aggregator('mode', statistics.mode, 'mode of values')
|
|
149
154
|
vd.aggregator('sum', vsum, 'sum of values')
|
150
155
|
vd.aggregator('distinct', set, 'distinct values', type=vlen)
|
151
156
|
vd.aggregator('count', lambda values: sum(1 for v in values), 'number of values', type=int)
|
152
|
-
vd.aggregator('list', list, 'list of values')
|
157
|
+
vd.aggregator('list', list, 'list of values', type=anytype)
|
153
158
|
vd.aggregator('stdev', statistics.stdev, 'standard deviation of values', type=float)
|
154
159
|
|
155
160
|
vd.aggregators['q3'] = quantiles(3, 'tertiles (33/66th pctile)')
|
@@ -160,10 +165,31 @@ vd.aggregators['q10'] = quantiles(10, 'deciles (10/20/30/40/50/60/70/80/90th pct
|
|
160
165
|
# since bb29b6e, a record of every aggregator
|
161
166
|
# is needed in vd.aggregators
|
162
167
|
for pct in (10, 20, 25, 30, 33, 40, 50, 60, 67, 70, 75, 80, 90, 95, 99):
|
163
|
-
vd.aggregators[f'p{pct}'] =
|
168
|
+
vd.aggregators[f'p{pct}'] = PercentileAggregator(pct, f'{pct}th percentile')
|
169
|
+
|
170
|
+
class KeyFindingAggregator(Aggregator):
|
171
|
+
'''Return the key of the row that results from applying *aggr_func* to *rows*.
|
172
|
+
Return None if *rows* is an empty list.
|
173
|
+
*aggr_func* takes a list of (value, row) tuples, one for each row in the column,
|
174
|
+
excluding rows where the column holds null and error values.
|
175
|
+
*aggr_func* must also take the parameters *default* and *key*, as max() does:
|
176
|
+
https://docs.python.org/3/library/functions.html#max'''
|
164
177
|
|
165
|
-
|
166
|
-
|
178
|
+
def __init__(self, aggr_func, *args, **kwargs):
|
179
|
+
self.aggr_func = aggr_func
|
180
|
+
super().__init__(*args, **kwargs)
|
181
|
+
|
182
|
+
def aggregate(self, col, rows):
|
183
|
+
if not col.sheet.keyCols:
|
184
|
+
vd.error('key aggregator function requires one or more key columns')
|
185
|
+
return None
|
186
|
+
# convert dicts to lists because functions like max() can't compare dicts
|
187
|
+
sortkey = lambda t: (t[0], sorted(t[1].items())) if isinstance(t[1], dict) else t
|
188
|
+
row = self.aggr_func(col.getValueRows(rows), default=(None, None), key=sortkey)[1]
|
189
|
+
return col.sheet.rowkey(row) if row else None
|
190
|
+
|
191
|
+
vd.aggregators['keymin'] = KeyFindingAggregator(min, 'keymin', anytype, helpstr='key of the minimum value')
|
192
|
+
vd.aggregators['keymax'] = KeyFindingAggregator(max, 'keymax', anytype, helpstr='key of the maximum value')
|
167
193
|
|
168
194
|
|
169
195
|
ColumnsSheet.columns += [
|
@@ -175,7 +201,7 @@ ColumnsSheet.columns += [
|
|
175
201
|
|
176
202
|
@Sheet.api
|
177
203
|
def addAggregators(sheet, cols, aggrnames):
|
178
|
-
'Add each aggregator in list of *aggrnames* to each of *cols*.'
|
204
|
+
'Add each aggregator in list of *aggrnames* to each of *cols*. Ignores names that are not valid.'
|
179
205
|
for aggrname in aggrnames:
|
180
206
|
aggrs = vd.aggregators.get(aggrname)
|
181
207
|
aggrs = aggrs if isinstance(aggrs, list) else [aggrs]
|
@@ -192,16 +218,35 @@ def aggname(col, agg):
|
|
192
218
|
'Consistent formatting of the name of given aggregator for this column. e.g. "col1_sum"'
|
193
219
|
return '%s_%s' % (col.name, agg.name)
|
194
220
|
|
221
|
+
@Column.api
|
222
|
+
def aggregateTotal(col, agg):
|
223
|
+
if agg not in col._aggregatedTotals:
|
224
|
+
col._aggregatedTotals[agg] = INPROGRESS
|
225
|
+
col._aggregateTotalAsync(agg)
|
226
|
+
return col._aggregatedTotals[agg]
|
227
|
+
|
228
|
+
|
229
|
+
@Column.api
|
230
|
+
@asyncthread
|
231
|
+
def _aggregateTotalAsync(col, agg):
|
232
|
+
col._aggregatedTotals[agg] = agg.aggregate(col, col.sheet.rows)
|
233
|
+
|
234
|
+
|
195
235
|
@Column.api
|
196
236
|
@asyncthread
|
197
|
-
def memo_aggregate(col,
|
237
|
+
def memo_aggregate(col, agg_choices, rows):
|
198
238
|
'Show aggregated value in status, and add to memory.'
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
239
|
+
for agg_choice in agg_choices:
|
240
|
+
agg = vd.aggregators.get(agg_choice)
|
241
|
+
if not agg: continue
|
242
|
+
aggs = agg if isinstance(agg, list) else [agg]
|
243
|
+
for agg in aggs:
|
244
|
+
aggval = agg.aggregate(col, rows)
|
245
|
+
typedval = wrapply(agg.type or col.type, aggval)
|
246
|
+
dispval = col.format(typedval)
|
247
|
+
k = col.name+'_'+agg.name
|
248
|
+
vd.status(f'{k}={dispval}')
|
249
|
+
vd.memory[k] = typedval
|
205
250
|
|
206
251
|
|
207
252
|
@VisiData.property
|
@@ -215,6 +260,7 @@ def aggregator_choices(vd):
|
|
215
260
|
|
216
261
|
@VisiData.api
|
217
262
|
def chooseAggregators(vd):
|
263
|
+
'''Return a list of aggregator name strings chosen or entered by the user. User-entered names may be invalid.'''
|
218
264
|
prompt = 'choose aggregators: '
|
219
265
|
def _fmt_aggr_summary(match, row, trigger_key):
|
220
266
|
formatted_aggrname = match.formatted.get('key', row.key) if match else row.key
|
@@ -235,12 +281,15 @@ def chooseAggregators(vd):
|
|
235
281
|
multiple=True)
|
236
282
|
|
237
283
|
aggrs = r.split()
|
284
|
+
valid_choices = vd.aggregators.keys()
|
238
285
|
for aggr in aggrs:
|
239
286
|
vd.usedInputs[aggr] += 1
|
287
|
+
if aggr not in valid_choices:
|
288
|
+
vd.warning(f'aggregator does not exist: {aggr}')
|
240
289
|
return aggrs
|
241
290
|
|
242
|
-
Sheet.addCommand('+', 'aggregate-col', 'addAggregators([cursorCol], chooseAggregators())', '
|
243
|
-
Sheet.addCommand('z+', 'memo-aggregate', '
|
291
|
+
Sheet.addCommand('+', 'aggregate-col', 'addAggregators([cursorCol], chooseAggregators())', 'add aggregator to current column')
|
292
|
+
Sheet.addCommand('z+', 'memo-aggregate', 'cursorCol.memo_aggregate(chooseAggregators(), selectedRows or rows)', 'memo result of aggregator over values in selected rows for current column')
|
244
293
|
ColumnsSheet.addCommand('g+', 'aggregate-cols', 'addAggregators(selectedRows or source[0].nonKeyVisibleCols, chooseAggregators())', 'add aggregators to selected source columns')
|
245
294
|
|
246
295
|
vd.addMenuItems('''
|
visidata/apps/vdsql/_ibis.py
CHANGED
@@ -60,6 +60,8 @@ def configure_ibis(vd):
|
|
60
60
|
|
61
61
|
@VisiData.api
|
62
62
|
def open_vdsql(vd, p, filetype=None):
|
63
|
+
import ibis
|
64
|
+
|
63
65
|
vd.configure_ibis()
|
64
66
|
|
65
67
|
# on-demand aliasing, so we don't need deps for all backends
|
@@ -70,7 +72,9 @@ def open_vdsql(vd, p, filetype=None):
|
|
70
72
|
return IbisTableIndexSheet(p.base_stem, source=p, filetype=None, database_name=None,
|
71
73
|
ibis_conpool=IbisConnectionPool(p), sheet_type=IbisTableSheet)
|
72
74
|
|
75
|
+
|
73
76
|
vd.open_ibis = vd.open_vdsql
|
77
|
+
vd.openurl_sqlite = vd.open_vdsql
|
74
78
|
|
75
79
|
|
76
80
|
class IbisConnectionPool:
|
@@ -85,14 +89,8 @@ class IbisConnectionPool:
|
|
85
89
|
@contextmanager
|
86
90
|
def get_conn(self):
|
87
91
|
if not self.pool:
|
88
|
-
import
|
89
|
-
|
90
|
-
import ibis
|
91
|
-
r = ibis.connect(str(self.source))
|
92
|
-
except sqlalchemy.exc.NoSuchModuleError as e:
|
93
|
-
dialect = str(e).split(':')[-1]
|
94
|
-
vd.warning(f'{dialect} not installed')
|
95
|
-
vd.fail(f'pip install ibis-framework[{dialect}]')
|
92
|
+
import ibis
|
93
|
+
r = ibis.connect(str(self.source))
|
96
94
|
else:
|
97
95
|
r = self.pool.pop(0)
|
98
96
|
|
@@ -210,9 +208,9 @@ class IbisTableSheet(Sheet):
|
|
210
208
|
def curcol_sql(self):
|
211
209
|
expr = self.cursorCol.get_ibis_col(self.ibis_current_expr)
|
212
210
|
if expr is not None:
|
213
|
-
return self.
|
211
|
+
return self.ibis_expr_to_sql(expr, fragment=True)
|
214
212
|
|
215
|
-
def
|
213
|
+
def ibis_expr_to_sql(self, expr, fragment=False):
|
216
214
|
import sqlparse
|
217
215
|
with self.con as con:
|
218
216
|
context = con.compiler.make_context()
|
@@ -323,7 +321,7 @@ class IbisTableSheet(Sheet):
|
|
323
321
|
def sqlize(self, expr):
|
324
322
|
if vd.options.debug:
|
325
323
|
expr = self.withRowcount(expr)
|
326
|
-
return self.
|
324
|
+
return self.ibis_expr_to_sql(expr)
|
327
325
|
|
328
326
|
@property
|
329
327
|
def substrait(self):
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import time
|
2
2
|
|
3
|
-
from visidata import
|
3
|
+
from visidata import vd, VisiData, Progress
|
4
4
|
|
5
5
|
from ._ibis import IbisTableSheet, IbisTableIndexSheet, IbisConnectionPool
|
6
6
|
|
@@ -45,7 +45,7 @@ class ClickhouseSheet(IbisTableSheet):
|
|
45
45
|
raise
|
46
46
|
except BaseException:
|
47
47
|
if qid:
|
48
|
-
con.
|
48
|
+
con._client.cancel(qid)
|
49
49
|
|
50
50
|
|
51
51
|
ClickhouseSheet.init('total_rows', lambda: None)
|
visidata/apps/vdsql/snowflake.py
CHANGED
visidata/apps/vgit/status.py
CHANGED
@@ -112,7 +112,7 @@ class GitStatus(GitSheet):
|
|
112
112
|
return self.gitBranchStatuses.get(self.branch, 'no branch')
|
113
113
|
|
114
114
|
def iterload(self):
|
115
|
-
files = [GitFile(p, self.source) for p in self.source.iterdir() if p.
|
115
|
+
files = [GitFile(p, self.source) for p in self.source.iterdir() if p.stem not in ('.git')] # files in working dir
|
116
116
|
|
117
117
|
filenames = dict((gf.filename, gf) for gf in files)
|
118
118
|
|
visidata/basesheet.py
CHANGED
@@ -67,6 +67,10 @@ class DrawablePane(Extensible):
|
|
67
67
|
'Width of the current sheet window, in single-width characters.'
|
68
68
|
return self._scr.getmaxyx()[1] if self._scr else 80
|
69
69
|
|
70
|
+
@property
|
71
|
+
def currow(self):
|
72
|
+
return None
|
73
|
+
|
70
74
|
def execCommand2(self, cmd, vdglobals=None):
|
71
75
|
"Execute `cmd` with `vdglobals` as globals and this sheet's attributes as locals. Return True if user cancelled."
|
72
76
|
|
@@ -112,9 +116,9 @@ class BaseSheet(DrawablePane):
|
|
112
116
|
|
113
117
|
def __init__(self, *names, rows=UNLOADED, **kwargs):
|
114
118
|
self._name = None # initial cache value necessary for self.options
|
119
|
+
self._names = []
|
115
120
|
self.loading = False
|
116
121
|
self.names = list(names)
|
117
|
-
self.name = self.options.name_joiner.join(str(x) for x in self.names if x)
|
118
122
|
self.source = None
|
119
123
|
self.rows = rows # list of opaque objects
|
120
124
|
self._scr = None
|
@@ -194,7 +198,7 @@ class BaseSheet(DrawablePane):
|
|
194
198
|
|
195
199
|
cmd = self.getCommand(longname or keystrokes)
|
196
200
|
if not cmd:
|
197
|
-
vd.
|
201
|
+
vd.fail('no command for %s' % (longname or keystrokes))
|
198
202
|
return False
|
199
203
|
|
200
204
|
escaped = False
|
@@ -238,8 +242,10 @@ class BaseSheet(DrawablePane):
|
|
238
242
|
|
239
243
|
@names.setter
|
240
244
|
def names(self, names):
|
245
|
+
if self._names:
|
246
|
+
vd.addUndo(setattr, self, 'names', self._names)
|
241
247
|
self._names = names
|
242
|
-
self.
|
248
|
+
self._name = self.options.name_joiner.join(self.maybeClean(str(x)) for x in self._names)
|
243
249
|
|
244
250
|
@property
|
245
251
|
def name(self):
|
@@ -250,8 +256,9 @@ class BaseSheet(DrawablePane):
|
|
250
256
|
def name(self, name):
|
251
257
|
'Set name without spaces.'
|
252
258
|
if self._names:
|
253
|
-
vd.addUndo(setattr, self, '
|
259
|
+
vd.addUndo(setattr, self, 'names', self._names)
|
254
260
|
self._name = self.maybeClean(str(name))
|
261
|
+
self._names = [self._name]
|
255
262
|
|
256
263
|
def maybeClean(self, s):
|
257
264
|
'stub'
|
visidata/canvas.py
CHANGED
@@ -145,7 +145,7 @@ class Plotter(BaseSheet):
|
|
145
145
|
self.labels = [] # (x, y, text, attr, row)
|
146
146
|
self.hiddenAttrs = set()
|
147
147
|
self.needsRefresh = False
|
148
|
-
self.resetCanvasDimensions(
|
148
|
+
self.resetCanvasDimensions(1, 1) #2171
|
149
149
|
|
150
150
|
@property
|
151
151
|
def nRows(self):
|
@@ -209,15 +209,23 @@ class Plotter(BaseSheet):
|
|
209
209
|
self.hiddenAttrs.remove(attr)
|
210
210
|
self.plotlegends()
|
211
211
|
|
212
|
-
def rowsWithin(self, plotter_bbox):
|
212
|
+
def rowsWithin(self, plotter_bbox, invert_y=False):
|
213
213
|
'return list of deduped rows within plotter_bbox'
|
214
214
|
ret = {}
|
215
|
+
|
215
216
|
x_start = max(0, plotter_bbox.xmin)
|
217
|
+
if len(self.pixels) == 0: return []
|
218
|
+
x_end = min(len(self.pixels[0]), plotter_bbox.xmax)
|
219
|
+
|
216
220
|
y_start = max(0, plotter_bbox.ymin)
|
217
221
|
y_end = min(len(self.pixels), plotter_bbox.ymax)
|
218
|
-
|
219
|
-
|
220
|
-
|
222
|
+
if invert_y:
|
223
|
+
y_range = range(y_end-1, y_start-1, -1)
|
224
|
+
else:
|
225
|
+
y_range = range(y_start, y_end)
|
226
|
+
|
227
|
+
for x in range(x_start, x_end):
|
228
|
+
for y in y_range:
|
221
229
|
for attr, rows in self.pixels[y][x].items():
|
222
230
|
if attr not in self.hiddenAttrs:
|
223
231
|
for r in rows:
|
@@ -226,12 +234,28 @@ class Plotter(BaseSheet):
|
|
226
234
|
|
227
235
|
def draw(self, scr):
|
228
236
|
windowHeight, windowWidth = scr.getmaxyx()
|
229
|
-
disp_canvas_charset = self.options.disp_canvas_charset or ' o'
|
230
|
-
disp_canvas_charset += (256 - len(disp_canvas_charset)) * disp_canvas_charset[-1]
|
231
|
-
|
232
237
|
if self.needsRefresh:
|
233
238
|
self.render(windowHeight, windowWidth)
|
234
239
|
|
240
|
+
self.draw_pixels(scr)
|
241
|
+
self.draw_labels(scr)
|
242
|
+
|
243
|
+
def draw_empty(self, scr):
|
244
|
+
# use draw_empty() when calling draw_pixels() with clear_empty_squares=False
|
245
|
+
cursorBBox = self.plotterCursorBox
|
246
|
+
for char_y in range(0, self.plotheight//4):
|
247
|
+
for char_x in range(0, self.plotwidth//2):
|
248
|
+
cattr = ColorAttr()
|
249
|
+
ch = ' '
|
250
|
+
# draw cursor
|
251
|
+
if cursorBBox.contains(char_x*2, char_y*4) or \
|
252
|
+
cursorBBox.contains(char_x*2+1, char_y*4+3):
|
253
|
+
cattr = update_attr(cattr, colors.color_current_row)
|
254
|
+
scr.addstr(char_y, char_x, ch, cattr.attr)
|
255
|
+
|
256
|
+
def draw_pixels(self, scr, clear_empty_squares=True):
|
257
|
+
disp_canvas_charset = self.options.disp_canvas_charset or ' o'
|
258
|
+
disp_canvas_charset += (256 - len(disp_canvas_charset)) * disp_canvas_charset[-1]
|
235
259
|
if self.pixels:
|
236
260
|
cursorBBox = self.plotterCursorBox
|
237
261
|
getPixelAttr = self.getPixelAttrRandom if self.options.disp_pixel_random else self.getPixelAttrMost
|
@@ -256,19 +280,26 @@ class Plotter(BaseSheet):
|
|
256
280
|
braille_num += pow2
|
257
281
|
pow2 *= 2
|
258
282
|
|
283
|
+
ch = disp_canvas_charset[braille_num]
|
259
284
|
if braille_num != 0:
|
260
285
|
color = Counter(c for c in block_attrs if c).most_common(1)[0][0]
|
261
286
|
cattr = colors.get_color(color)
|
262
287
|
else:
|
263
288
|
cattr = ColorAttr()
|
289
|
+
# don't erase empty squares, useful for subclasses that draw elements like reflines
|
290
|
+
# before pixels are drawn
|
291
|
+
if not clear_empty_squares:
|
292
|
+
continue
|
264
293
|
|
294
|
+
# draw cursor
|
265
295
|
if cursorBBox.contains(char_x*2, char_y*4) or \
|
266
296
|
cursorBBox.contains(char_x*2+1, char_y*4+3):
|
267
297
|
cattr = update_attr(cattr, colors.color_current_row)
|
268
298
|
|
269
299
|
if cattr.attr:
|
270
|
-
scr.addstr(char_y, char_x,
|
300
|
+
scr.addstr(char_y, char_x, ch, cattr.attr)
|
271
301
|
|
302
|
+
def draw_labels(self, scr):
|
272
303
|
def _mark_overlap_text(labels, textobj):
|
273
304
|
def _overlaps(a, b):
|
274
305
|
a_x1, _, a_txt, _, _ = a
|
@@ -310,7 +341,7 @@ class Plotter(BaseSheet):
|
|
310
341
|
cursorBBox = self.plotterCursorBox
|
311
342
|
for c in txt:
|
312
343
|
w = dispwidth(c)
|
313
|
-
#
|
344
|
+
# draw cursor if the cursor contains the midpoint of the character cell
|
314
345
|
if cursorBBox.contains(char_x*2+1, char_y*4+2):
|
315
346
|
char_attr = update_attr(cattr, colors.color_current_row)
|
316
347
|
clipdraw(scr, char_y, char_x, c, char_attr, w)
|
@@ -385,6 +416,12 @@ class Canvas(Plotter):
|
|
385
416
|
if self.cursorBox.xmin == self.visibleBox.xmin and self.cursorBox.ymin == self.calcBottomCursorY():
|
386
417
|
realign_cursor = True
|
387
418
|
super().resetCanvasDimensions(windowHeight, windowWidth)
|
419
|
+
# if window is not big enough to contain a particular margin, pretend that margin is 0
|
420
|
+
pvbox_x = pvbox_y = 0
|
421
|
+
if self.plotwidth > self.left_margin:
|
422
|
+
pvbox_x = self.left_margin
|
423
|
+
if self.plotheight > self.topMarginPixels:
|
424
|
+
pvbox_y = self.topMarginPixels
|
388
425
|
if hasattr(self, 'legendwidth'):
|
389
426
|
# +4 = 1 empty space after the graph + 2 characters for the legend prefixes of "1:", "2:", etc +
|
390
427
|
# 1 character for the empty rightmost column
|
@@ -395,8 +432,10 @@ class Canvas(Plotter):
|
|
395
432
|
else:
|
396
433
|
pvbox_xmax = self.plotwidth-self.rightMarginPixels-1
|
397
434
|
self.left_margin = min(self.left_margin, math.ceil(self.plotwidth * 1/3)//2*2)
|
398
|
-
|
399
|
-
|
435
|
+
# enforce a minimum plotview box size of 1x1
|
436
|
+
pvbox_xmax = max(pvbox_xmax, 1)
|
437
|
+
pvbox_ymax = max(self.plotheight-self.bottomMarginPixels-1, 1)
|
438
|
+
self.plotviewBox = BoundingBox(pvbox_x, pvbox_y, pvbox_xmax, pvbox_ymax)
|
400
439
|
if [self.plotheight, self.plotwidth] != old_plotsize:
|
401
440
|
if hasattr(self, 'cursorBox') and self.cursorBox:
|
402
441
|
self.setCursorSizeInPlotterPixels(2, 4)
|
@@ -513,13 +552,14 @@ class Canvas(Plotter):
|
|
513
552
|
'adjust visibleBox.xymin so that canvasPoint is plotted at plotterPoint'
|
514
553
|
self.visibleBox.xmin = canvasPoint.x - self.canvasW(plotterPoint.x-self.plotviewBox.xmin)
|
515
554
|
self.visibleBox.ymin = canvasPoint.y - self.canvasH(plotterPoint.y-self.plotviewBox.ymin)
|
516
|
-
self.
|
555
|
+
self.resetBounds()
|
517
556
|
|
518
557
|
def zoomTo(self, bbox):
|
519
558
|
'set visible area to bbox, maintaining aspectRatio if applicable'
|
520
559
|
self.fixPoint(self.plotviewBox.xymin, bbox.xymin)
|
521
560
|
self.xzoomlevel=bbox.w/self.canvasBox.w
|
522
561
|
self.yzoomlevel=bbox.h/self.canvasBox.h
|
562
|
+
self.resetBounds()
|
523
563
|
|
524
564
|
def incrZoom(self, incr):
|
525
565
|
self.xzoomlevel *= incr
|
@@ -527,7 +567,7 @@ class Canvas(Plotter):
|
|
527
567
|
|
528
568
|
self.resetBounds()
|
529
569
|
|
530
|
-
def resetBounds(self):
|
570
|
+
def resetBounds(self, refresh=True):
|
531
571
|
'create canvasBox and cursorBox if necessary, and set visibleBox w/h according to zoomlevels. then redisplay legends.'
|
532
572
|
if not self.canvasBox:
|
533
573
|
xmin, ymin, xmax, ymax = None, None, None, None
|
@@ -564,6 +604,8 @@ class Canvas(Plotter):
|
|
564
604
|
self.cursorBox = Box(cb_xmin, cb_ymin, self.canvasCharWidth, self.canvasCharHeight)
|
565
605
|
|
566
606
|
self.plotlegends()
|
607
|
+
if refresh:
|
608
|
+
self.refresh()
|
567
609
|
|
568
610
|
def calcTopCursorY(self):
|
569
611
|
'ymin for the cursor that will align its top with the top edge of the graph'
|
@@ -641,13 +683,13 @@ class Canvas(Plotter):
|
|
641
683
|
else:
|
642
684
|
return h
|
643
685
|
|
644
|
-
def scaleX(self, canvasX):
|
686
|
+
def scaleX(self, canvasX) -> int:
|
645
687
|
'returns a plotter x coordinate'
|
646
|
-
return
|
688
|
+
return self.plotviewBox.xmin+round((canvasX-self.visibleBox.xmin)*self.xScaler)
|
647
689
|
|
648
|
-
def scaleY(self, canvasY):
|
690
|
+
def scaleY(self, canvasY) -> int:
|
649
691
|
'returns a plotter y coordinate'
|
650
|
-
return
|
692
|
+
return self.plotviewBox.ymin+round((canvasY-self.visibleBox.ymin)*self.yScaler)
|
651
693
|
|
652
694
|
def unscaleX(self, plotterX):
|
653
695
|
'performs the inverse of scaleX, returns a canvas x coordinate'
|
@@ -675,6 +717,7 @@ class Canvas(Plotter):
|
|
675
717
|
vd.cancelThread(*(t for t in self.currentThreads if t.name == 'plotAll_async'))
|
676
718
|
self.labels.clear()
|
677
719
|
self.resetCanvasDimensions(h, w)
|
720
|
+
self.resetBounds(refresh=False)
|
678
721
|
self.render_async()
|
679
722
|
|
680
723
|
@asyncthread
|
@@ -701,12 +744,12 @@ class Canvas(Plotter):
|
|
701
744
|
x1, y1 = float(x1), float(y1)
|
702
745
|
if xmin <= x1 <= xmax and ymin <= y1 <= ymax:
|
703
746
|
# equivalent to self.scaleX(x1) and self.scaleY(y1), inlined for speed
|
704
|
-
x = plotxmin+(x1-xmin)*xfactor
|
747
|
+
x = plotxmin+round((x1-xmin)*xfactor)
|
705
748
|
if invert_y:
|
706
|
-
y = plotymax-(y1-ymin)*yfactor
|
749
|
+
y = plotymax-round((y1-ymin)*yfactor)
|
707
750
|
else:
|
708
|
-
y = plotymin+(y1-ymin)*yfactor
|
709
|
-
self.plotpixel(
|
751
|
+
y = plotymin+round((y1-ymin)*yfactor)
|
752
|
+
self.plotpixel(x, y, attr, row)
|
710
753
|
continue
|
711
754
|
|
712
755
|
prev_x, prev_y = vertexes[0]
|
@@ -735,7 +778,6 @@ class Canvas(Plotter):
|
|
735
778
|
self.source.deleteBy(lambda r,rows=rows: r in rows)
|
736
779
|
self.reload()
|
737
780
|
|
738
|
-
|
739
781
|
Plotter.addCommand('v', 'visibility', 'options.disp_graph_labels = not options.disp_graph_labels', 'toggle disp_graph_labels option')
|
740
782
|
|
741
783
|
Canvas.addCommand(None, 'go-left', 'if cursorBox: sheet.cursorBox.xmin -= cursorBox.w', 'move cursor left by its width')
|
@@ -768,7 +810,7 @@ Canvas.addCommand('zz', 'zoom-cursor', 'zoomTo(cursorBox)', 'set visible bounds
|
|
768
810
|
|
769
811
|
Canvas.addCommand('-', 'zoomout-cursor', 'tmp=cursorBox.center; incrZoom(options.disp_zoom_incr); fixPoint(plotviewBox.center, tmp)', 'zoom out from cursor center')
|
770
812
|
Canvas.addCommand('+', 'zoomin-cursor', 'tmp=cursorBox.center; incrZoom(1.0/options.disp_zoom_incr); fixPoint(plotviewBox.center, tmp)', 'zoom into cursor center')
|
771
|
-
Canvas.addCommand('_', 'zoom-all', 'sheet.canvasBox = None; sheet.visibleBox = None; sheet.xzoomlevel=sheet.yzoomlevel=1.0; resetBounds()
|
813
|
+
Canvas.addCommand('_', 'zoom-all', 'sheet.canvasBox = None; sheet.visibleBox = None; sheet.xzoomlevel=sheet.yzoomlevel=1.0; resetBounds()', 'zoom to fit full extent')
|
772
814
|
Canvas.addCommand('z_', 'set-aspect', 'sheet.aspectRatio = float(input("aspect ratio=", value=aspectRatio)); refresh()', 'set aspect ratio')
|
773
815
|
|
774
816
|
# set cursor box with left click
|