visidata 3.1.1__py3-none-any.whl → 3.2__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 +2 -2
- visidata/_input.py +70 -36
- visidata/_open.py +9 -6
- visidata/_types.py +2 -2
- visidata/aggregators.py +125 -16
- visidata/apps/vdsql/_ibis.py +8 -13
- visidata/basesheet.py +4 -1
- visidata/canvas.py +11 -7
- visidata/clipboard.py +11 -2
- visidata/cliptext.py +65 -23
- visidata/cmdlog.py +5 -1
- visidata/column.py +6 -2
- visidata/ddwplay.py +2 -2
- visidata/deprecated.py +91 -63
- visidata/errors.py +41 -5
- visidata/{features → experimental}/helloworld.py +1 -1
- visidata/expr.py +1 -0
- visidata/extensible.py +4 -0
- visidata/features/cmdpalette.py +3 -3
- visidata/features/describe.py +2 -2
- visidata/features/expand_cols.py +8 -5
- visidata/features/freeze.py +14 -2
- visidata/features/go_col.py +2 -1
- visidata/features/graph_zoom_y.py +47 -0
- visidata/features/incr.py +7 -3
- visidata/features/join.py +23 -12
- visidata/features/layout.py +8 -3
- visidata/features/melt.py +1 -0
- visidata/features/rank.py +103 -0
- visidata/features/reload_every.py +9 -6
- visidata/features/sysedit.py +14 -4
- visidata/features/transpose.py +1 -0
- visidata/features/window.py +12 -0
- visidata/form.py +4 -4
- visidata/freqtbl.py +47 -3
- visidata/fuzzymatch.py +8 -5
- visidata/graph.py +5 -3
- visidata/guides/AggregatorsSheet.md +84 -0
- visidata/guides/MacrosSheet.md +1 -1
- visidata/guides/RankGuide.md +51 -0
- visidata/guides/TypesSheet.md +1 -1
- visidata/guides/WindowFunctionGuide.md +49 -0
- visidata/help.py +3 -4
- visidata/indexsheet.py +1 -1
- visidata/loaders/_pandas.py +3 -1
- visidata/loaders/archive.py +6 -3
- visidata/loaders/csv.py +5 -1
- visidata/loaders/eml.py +2 -0
- visidata/loaders/f5log.py +2 -2
- visidata/loaders/fec.py +6 -9
- visidata/loaders/fixed_width.py +2 -0
- visidata/loaders/hdf5.py +34 -10
- visidata/loaders/npy.py +54 -23
- visidata/loaders/orgmode.py +3 -2
- visidata/loaders/pandas_freqtbl.py +4 -0
- visidata/loaders/psv.py +13 -0
- visidata/loaders/sqlite.py +1 -1
- visidata/loaders/vds.py +3 -4
- visidata/macros.py +4 -3
- visidata/main.py +11 -5
- visidata/mainloop.py +7 -4
- visidata/man/parse_options.py +3 -2
- visidata/man/vd.1 +26 -14
- visidata/man/vd.txt +25 -14
- visidata/menu.py +9 -9
- visidata/metasheets.py +3 -3
- visidata/mouse.py +1 -0
- visidata/pyobj.py +17 -9
- visidata/save.py +5 -1
- visidata/selection.py +29 -18
- visidata/settings.py +2 -2
- visidata/sheets.py +52 -24
- visidata/shell.py +2 -2
- visidata/sidebar.py +4 -2
- visidata/sort.py +89 -11
- visidata/statusbar.py +10 -9
- visidata/tests/test_cliptext.py +151 -0
- visidata/tests/test_commands.py +5 -2
- visidata/tests/test_menu.py +1 -1
- visidata/textsheet.py +34 -8
- visidata/themes/ascii8.py +2 -2
- visidata/themes/light.py +5 -0
- visidata/threads.py +16 -8
- visidata/undo.py +1 -1
- visidata/vendor/__init__.py +0 -0
- {visidata-3.1.1.data → visidata-3.2.data}/data/share/man/man1/vd.1 +26 -14
- {visidata-3.1.1.data → visidata-3.2.data}/data/share/man/man1/visidata.1 +26 -14
- {visidata-3.1.1.dist-info → visidata-3.2.dist-info}/METADATA +62 -15
- {visidata-3.1.1.dist-info → visidata-3.2.dist-info}/RECORD +95 -89
- {visidata-3.1.1.dist-info → visidata-3.2.dist-info}/WHEEL +1 -1
- {visidata-3.1.1.dist-info → visidata-3.2.dist-info}/entry_points.txt +1 -0
- visidata-3.1.1.data/scripts/vd +0 -6
- {visidata-3.1.1.data → visidata-3.2.data}/data/share/applications/visidata.desktop +0 -0
- {visidata-3.1.1.data → visidata-3.2.data}/scripts/vd2to3.vdx +0 -0
- {visidata-3.1.1.dist-info → visidata-3.2.dist-info}/LICENSE.gpl3 +0 -0
- {visidata-3.1.1.dist-info → visidata-3.2.dist-info}/top_level.txt +0 -0
visidata/features/describe.py
CHANGED
@@ -2,7 +2,7 @@ from copy import copy
|
|
2
2
|
from statistics import mode, median, mean, stdev
|
3
3
|
|
4
4
|
from visidata import vd, Column, ColumnAttr, vlen, RowColorizer, asyncthread, Progress, wrapply
|
5
|
-
from visidata import BaseSheet, TableSheet, ColumnsSheet,
|
5
|
+
from visidata import BaseSheet, TableSheet, ColumnsSheet, IndexSheet
|
6
6
|
|
7
7
|
|
8
8
|
vd.option('describe_aggrs', 'mean stdev', 'numeric aggregators to calculate on Describe sheet', help=vd.help_aggregators)
|
@@ -114,7 +114,7 @@ class DescribeSheet(ColumnsSheet):
|
|
114
114
|
|
115
115
|
TableSheet.addCommand('I', 'describe-sheet', 'vd.push(DescribeSheet(sheet.name+"_describe", source=[sheet]))', 'open Describe Sheet with descriptive statistics for all visible columns')
|
116
116
|
BaseSheet.addCommand('gI', 'describe-all', 'vd.push(DescribeSheet("describe_all", source=vd.stackedSheets))', 'open Describe Sheet with description statistics for all visible columns from all sheets')
|
117
|
-
|
117
|
+
IndexSheet.addCommand('gI', 'describe-selected', 'vd.push(DescribeSheet("describe_all", source=selectedRows))', 'open Describe Sheet with all visible columns from selected sheets')
|
118
118
|
|
119
119
|
DescribeSheet.addCommand('zs', 'select-cell', 'cursorRow.sheet.select(cursorValue)', 'select rows on source sheet which are being described in current cell')
|
120
120
|
DescribeSheet.addCommand('zu', 'unselect-cell', 'cursorRow.sheet.unselect(cursorValue)', 'unselect rows on source sheet which are being described in current cell')
|
visidata/features/expand_cols.py
CHANGED
@@ -121,8 +121,10 @@ class ExpandedColumn(Column):
|
|
121
121
|
def calcValue(self, row):
|
122
122
|
return getitemdef(self.origCol.getValue(row), self.expr)
|
123
123
|
|
124
|
-
def setValue(self, row, value):
|
124
|
+
def setValue(self, row, value, setModified=True):
|
125
125
|
self.origCol.getValue(row)[self.expr] = value
|
126
|
+
if setModified:
|
127
|
+
self.origCol.sheet.setModified()
|
126
128
|
|
127
129
|
|
128
130
|
@Sheet.api
|
@@ -141,6 +143,7 @@ def contract_cols(sheet, cols, depth=1): # depth == 0 means contract all the wa
|
|
141
143
|
col.width = sheet.options.default_width
|
142
144
|
|
143
145
|
sheet.columns = [col for col in sheet.columns if getattr(col, 'origCol', None) not in origCols]
|
146
|
+
sheet.calcColLayout()
|
144
147
|
|
145
148
|
|
146
149
|
@Sheet.api
|
@@ -181,10 +184,10 @@ Sheet.addCommand('g(', 'expand-cols', 'expand_cols_deep(visibleCols, depth=1)',
|
|
181
184
|
Sheet.addCommand('z(', 'expand-col-depth', 'expand_cols_deep([cursorCol], depth=int(input("expand depth=", value=0)))', 'expand current column of containers to given depth (0=fully)')
|
182
185
|
Sheet.addCommand('gz(', 'expand-cols-depth', 'expand_cols_deep(visibleCols, depth=int(input("expand depth=", value=0)))', 'expand all visible columns of containers to given depth (0=fully)')
|
183
186
|
|
184
|
-
Sheet.addCommand(')', 'contract-col', 'contract_cols([cursorCol])', 'remove current column and siblings from sheet columns and unhide parent')
|
185
|
-
Sheet.addCommand('g)', 'contract-cols', 'contract_cols(visibleCols)', 'remove all child columns and unhide toplevel parents')
|
186
|
-
Sheet.addCommand('z)', 'contract-col-depth', 'contract_cols([cursorCol], depth=int(input("contract depth=", value=0)))', 'remove current column and siblings from sheet columns and unhide parent')
|
187
|
-
Sheet.addCommand('gz)', 'contract-cols-depth', 'contract_cols(visibleCols, depth=int(input("contract depth=", value=0)))', 'remove all child columns and unhide toplevel parents')
|
187
|
+
Sheet.addCommand(')', 'contract-col', 'contract_cols([cursorCol])', 'remove current column and siblings from sheet columns and unhide parent (1 level)')
|
188
|
+
Sheet.addCommand('g)', 'contract-cols', 'contract_cols(visibleCols)', 'remove all child columns and unhide toplevel parents (1 level)')
|
189
|
+
Sheet.addCommand('z)', 'contract-col-depth', 'contract_cols([cursorCol], depth=int(input("contract depth=", value=0)))', 'remove current column and siblings from sheet columns and unhide parent, prompting for depth')
|
190
|
+
Sheet.addCommand('gz)', 'contract-cols-depth', 'contract_cols(visibleCols, depth=int(input("contract depth=", value=0)))', 'remove all child columns and unhide toplevel parents, prompting for depth')
|
188
191
|
|
189
192
|
ColumnsSheet.addCommand(')', 'contract-source-cols', 'source[0].addColumn(contract_source_cols(someSelectedRows), index=cursorRowIndex)', 'contract selected columns into column group') #1702
|
190
193
|
|
visidata/features/freeze.py
CHANGED
@@ -60,10 +60,22 @@ class StaticSheet(Sheet):
|
|
60
60
|
row.append(val)
|
61
61
|
|
62
62
|
|
63
|
+
@Sheet.api
|
64
|
+
def setcol_freeze(sheet, unfrozen): #2660 Contributed by @midichef
|
65
|
+
frozen = sheet.freeze_col(unfrozen)
|
66
|
+
frozen.name = unfrozen.name
|
67
|
+
unfrozen.hide()
|
68
|
+
vd.addUndoColNames([unfrozen])
|
69
|
+
unfrozen.name = frozen.name + '_unfrozen'
|
70
|
+
sheet.addColumnAtCursor(frozen)
|
71
|
+
vd.status(f'replaced {frozen.name} with frozen copy')
|
72
|
+
|
73
|
+
|
74
|
+
Sheet.addCommand("z'", 'setcol-freeze', 'setcol_freeze(cursorCol)', 'replace current column with a frozen copy, with all cells evaluated')
|
63
75
|
Sheet.addCommand("'", 'freeze-col', 'sheet.addColumnAtCursor(freeze_col(cursorCol))', 'add a frozen copy of current column with all cells evaluated')
|
64
76
|
Sheet.addCommand("g'", 'freeze-sheet', 'vd.push(StaticSheet(sheet)); status("pushed frozen copy of "+name)', 'open a frozen copy of current sheet with all visible columns evaluated')
|
65
|
-
Sheet.addCommand(
|
77
|
+
Sheet.addCommand(None, 'cache-col', 'cursorCol.resetCache()', 'add/reset cache for current column')
|
66
78
|
Sheet.addCommand("gz'", 'cache-cols', 'for c in visibleCols: c.resetCache()', 'add/reset cache for all visible columns')
|
67
79
|
|
68
|
-
vd.addMenuItem('Column', 'Freeze', 'freeze
|
80
|
+
vd.addMenuItem('Column', 'Freeze', 'setcol-freeze')
|
69
81
|
vd.addMenuItem('File', 'Freeze', 'freeze-sheet')
|
visidata/features/go_col.py
CHANGED
@@ -32,7 +32,7 @@ def nextColName(sheet, show_cells=True):
|
|
32
32
|
|
33
33
|
def _fmt_colname(match, row, trigger_key):
|
34
34
|
name = match.formatted.get('name', row.name) if match else row.name
|
35
|
-
r = ' '*(
|
35
|
+
r = ' '*(dispwidth(prompt)-3)
|
36
36
|
r += f'[:keystrokes]{trigger_key}[/] '
|
37
37
|
if show_cells and len(sheet.rows) > 0:
|
38
38
|
# pad the right side with spaces
|
@@ -46,6 +46,7 @@ def nextColName(sheet, show_cells=True):
|
|
46
46
|
else:
|
47
47
|
r += name
|
48
48
|
return r
|
49
|
+
|
49
50
|
name = vd.activeSheet.inputPalette(prompt,
|
50
51
|
colnames,
|
51
52
|
value_key='name',
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# contributed by Ram Rachum (@cool-RR) via ChatGPT #2751
|
2
|
+
|
3
|
+
from visidata import GraphSheet
|
4
|
+
|
5
|
+
@GraphSheet.api
|
6
|
+
def zoom_all_y(sheet):
|
7
|
+
"""Find the lowest and highest y values in the current visible plot and set the y range to that."""
|
8
|
+
ymin, ymax = None, None
|
9
|
+
|
10
|
+
# Find the min and max y values of all points within the current x range
|
11
|
+
xmin, xmax = sheet.visibleBox.xmin, sheet.visibleBox.xmax
|
12
|
+
|
13
|
+
for vertexes, attr, row in sheet.polylines:
|
14
|
+
if attr in sheet.hiddenAttrs:
|
15
|
+
continue
|
16
|
+
|
17
|
+
for x, y in vertexes:
|
18
|
+
# Check if the point is within the current x range
|
19
|
+
if xmin <= x <= xmax:
|
20
|
+
if ymin is None or y < ymin:
|
21
|
+
ymin = y
|
22
|
+
if ymax is None or y > ymax:
|
23
|
+
ymax = y
|
24
|
+
|
25
|
+
if ymin is None or ymax is None:
|
26
|
+
# No visible points found
|
27
|
+
return
|
28
|
+
|
29
|
+
# Add a 5% margin on both top and bottom
|
30
|
+
y_range = ymax - ymin
|
31
|
+
margin = sheet.ycols[0].type(y_range * 0.05)
|
32
|
+
|
33
|
+
# Calculate adjusted min/max with margin
|
34
|
+
adj_ymin = ymin - margin
|
35
|
+
adj_ymax = ymax + margin
|
36
|
+
|
37
|
+
# Create 5 equally spaced y ticks from real min to real max (not adjusted with margin)
|
38
|
+
step = (ymax - ymin) / 4 # 5 ticks means 4 intervals
|
39
|
+
y_ticks = tuple(ymin + step * i for i in range(5))
|
40
|
+
|
41
|
+
# Set the y range with margin
|
42
|
+
sheet.set_y(f"{adj_ymin} {adj_ymax}")
|
43
|
+
|
44
|
+
# Set custom y ticks (using the real min/max, not the adjusted ones)
|
45
|
+
sheet.forced_y_ticks = y_ticks
|
46
|
+
|
47
|
+
GraphSheet.addCommand('g_', 'zoom-all-y', 'zoom_all_y()', 'Zoom y-axis to fit all visible data points')
|
visidata/features/incr.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
from visidata import VisiData, Sheet, vd
|
1
|
+
from visidata import VisiData, Sheet, vd
|
2
2
|
|
3
3
|
|
4
4
|
vd.option('incr_base', 1.0, 'start value for column increments', replay=True)
|
@@ -7,10 +7,14 @@ vd.option('incr_base', 1.0, 'start value for column increments', replay=True)
|
|
7
7
|
@VisiData.api
|
8
8
|
def numrange(vd, n, step=1):
|
9
9
|
'Generate n values, starting from options.incr_base and increasing by step for each number.'
|
10
|
-
base = type(step)(options.incr_base)
|
11
|
-
yield from (base+x*step for x in range(n))
|
10
|
+
base = type(step)(vd.options.incr_base)
|
11
|
+
yield from ((base+x)*step for x in range(n))
|
12
12
|
|
13
13
|
|
14
|
+
def test_numrange(vd=None):
|
15
|
+
assert list(vd.numrange(5)) == [1,2,3,4,5]
|
16
|
+
assert list(vd.numrange(5, step=5)) == [5,10,15,20,25] #2769
|
17
|
+
|
14
18
|
@VisiData.api
|
15
19
|
def num(vd, *args):
|
16
20
|
'Return parsed string as number, preferring int to float.'
|
visidata/features/join.py
CHANGED
@@ -3,7 +3,7 @@ import itertools
|
|
3
3
|
import functools
|
4
4
|
from copy import copy
|
5
5
|
|
6
|
-
from visidata import vd, VisiData, asyncthread, Sheet, Progress, IndexSheet, Column, CellColorizer, ColumnItem, SubColumnItem, TypedWrapper, ColumnsSheet, AttrDict
|
6
|
+
from visidata import vd, VisiData, asyncthread, Sheet, Progress, IndexSheet, Column, CellColorizer, ColumnItem, SubColumnItem, TypedWrapper, ColumnsSheet, AttrDict, dispwidth
|
7
7
|
|
8
8
|
vd.help_join = '# Join Help\nHELPTODO'
|
9
9
|
|
@@ -166,7 +166,8 @@ class MergeColumn(Column):
|
|
166
166
|
|
167
167
|
def putValue(self, row, value):
|
168
168
|
for vs, c in reversed(list(self.cols.items())):
|
169
|
-
|
169
|
+
if row[vs] is not None:
|
170
|
+
c.setValue(row[vs], value)
|
170
171
|
|
171
172
|
def isDiff(self, row, value):
|
172
173
|
col = list(self.cols.values())[0]
|
@@ -362,11 +363,11 @@ class ConcatSheet(Sheet):
|
|
362
363
|
|
363
364
|
|
364
365
|
@VisiData.api
|
365
|
-
def
|
366
|
+
def inputJointype(vd):
|
366
367
|
prompt = 'choose jointype: '
|
367
368
|
def _fmt_aggr_summary(match, row, trigger_key):
|
368
369
|
formatted_jointype = match.formatted.get('key', row.key) if match else row.key
|
369
|
-
r = ' '*(
|
370
|
+
r = ' '*(dispwidth(prompt)-3)
|
370
371
|
r += f'[:keystrokes]{trigger_key}[/] '
|
371
372
|
r += formatted_jointype
|
372
373
|
if row.desc:
|
@@ -382,19 +383,29 @@ def chooseJointype(vd):
|
|
382
383
|
type='jointype')
|
383
384
|
|
384
385
|
|
385
|
-
IndexSheet.addCommand('&', 'join-selected', 'left, rights = someSelectedRows[0], someSelectedRows[1:]; vd.push(left.openJoin(rights, jointype=
|
386
|
+
IndexSheet.addCommand('&', 'join-selected', 'left, rights = someSelectedRows[0], someSelectedRows[1:]; vd.push(left.openJoin(rights, jointype=inputJointype()))', 'join selected sheets with visible columns from all, keeping rows according to jointype')
|
386
387
|
IndexSheet.bindkey('g&', 'join-selected')
|
387
|
-
Sheet.addCommand('&', 'join-sheets-top2', 'vd.push(openJoin(vd.sheets[1:2], jointype=
|
388
|
-
Sheet.addCommand('g&', 'join-sheets-all', 'vd.push(openJoin(vd.sheets[1:], jointype=
|
389
|
-
|
390
|
-
ColumnsSheet.addCommand('&', 'join-sheets-cols', 'vd.push(join_sheets_cols(selectedRows, jointype=chooseJointype()))', '')
|
388
|
+
Sheet.addCommand('&', 'join-sheets-top2', 'vd.push(openJoin(vd.sheets[1:2], jointype=inputJointype()))', 'join top two sheets on Sheets Stack')
|
389
|
+
Sheet.addCommand('g&', 'join-sheets-all', 'vd.push(openJoin(vd.sheets[1:], jointype=inputJointype()))', 'join all sheets on Sheets Stack')
|
390
|
+
ColumnsSheet.addCommand('&', 'join-sheets-cols', 'vd.push(join_sheets_cols(selectedRows, jointype=inputJointype()))', 'join sheets for selected columns')
|
391
391
|
|
392
392
|
vd.addMenuItems('''
|
393
|
-
Data > Join >
|
394
|
-
Data > Join >
|
395
|
-
Data > Join >
|
393
|
+
Data > Join > Selected Sheets > choose jointype > join-selected
|
394
|
+
Data > Join > Top Two Sheets > choose jointype > join-sheets-top2
|
395
|
+
Data > Join > All Sheets > choose jointype > join-sheets-all
|
396
396
|
''')
|
397
397
|
|
398
|
+
for d in vd.jointypes:
|
399
|
+
jointype, joinhelp = d.key, d.desc
|
400
|
+
IndexSheet.addCommand('', f'join-selected-{jointype}', 'left, rights = someSelectedRows[0], someSelectedRows[1:]; vd.push(left.openJoin(rights, jointype="{jointype}))', f'join selected sheets, keeping {joinhelp}')
|
401
|
+
Sheet.addCommand('', f'join-sheets-top2-{jointype}', f'vd.push(openJoin(vd.sheets[1:2], jointype="{jointype}"))', f'join top two sheets on Sheets Stack, keeping {joinhelp}')
|
402
|
+
Sheet.addCommand('', f'join-sheets-all-{jointype}', f'vd.push(openJoin(vd.sheets[1:], jointype="{jointype}"))', f'join all sheets on Sheets Stack, keeping {joinhelp}')
|
403
|
+
ColumnsSheet.addCommand('', 'join-cols-{jointype}', 'vd.push(join_sheets_cols(selectedRows, jointype=inputJointype()))', f'join sheets for selected columns, keeping {joinhelp}')
|
404
|
+
|
405
|
+
vd.addMenuItems(f'''Data > Join > Selected Sheets > {jointype} > join-selected-{jointype}''')
|
406
|
+
vd.addMenuItems(f'''Data > Join > Top Two Sheets > {jointype} > join-sheets-top2-{jointype}''')
|
407
|
+
vd.addMenuItems(f'''Data > Join > All Sheets > {jointype} > join-selected-{jointype}''')
|
408
|
+
|
398
409
|
IndexSheet.guide += '''
|
399
410
|
- `&` to join the selected sheets together
|
400
411
|
'''
|
visidata/features/layout.py
CHANGED
@@ -40,20 +40,25 @@ def hide_col(vd, col):
|
|
40
40
|
def hide_uniform_cols(sheet):
|
41
41
|
if len(sheet.rows) < 2:
|
42
42
|
return
|
43
|
-
|
43
|
+
idx = sheet.cursorVisibleColIndex
|
44
|
+
for i, col in enumerate(sheet.visibleCols):
|
44
45
|
vals = (col.getTypedValue(r) for r in sheet.rows)
|
45
46
|
first = next(vals)
|
46
47
|
if all(v == first for v in vals):
|
48
|
+
vd.status(f'hide {col.name} with value: {col.getDisplayValue(sheet.rows[0])}')
|
49
|
+
if i <= idx:
|
50
|
+
sheet.cursorRight(-1)
|
47
51
|
col.hide()
|
52
|
+
Sheet.clear_all_caches() #2578
|
48
53
|
|
49
54
|
Sheet.addCommand('_', 'resize-col-max', 'if cursorCol: cursorCol.toggleWidth(cursorCol.getMaxWidth(visibleRows))', 'toggle width of current column between full and default width')
|
50
55
|
Sheet.addCommand('z_', 'resize-col-input', 'width = int(input("set width= ", value=cursorCol.width)); cursorCol.setWidth(width)', 'adjust width of current column to N')
|
51
|
-
Sheet.addCommand('g_', 'resize-cols-max', 'for c in visibleCols: c.
|
56
|
+
Sheet.addCommand('g_', 'resize-cols-max', 'for c in visibleCols: c.toggleWidth(c.getMaxWidth(visibleRows))', 'toggle widths of all visible columns between full and default width')
|
52
57
|
Sheet.addCommand('gz_', 'resize-cols-input', 'width = int(input("set width= ", value=cursorCol.width)); Fanout(visibleCols).setWidth(width)', 'adjust widths of all visible columns to N')
|
53
58
|
|
54
59
|
Sheet.addCommand('-', 'hide-col', 'hide_col(cursorCol)', 'hide the current column')
|
55
60
|
Sheet.addCommand('z-', 'resize-col-half', 'cursorCol.setWidth(cursorCol.width//2)', 'reduce width of current column by half')
|
56
|
-
Sheet.addCommand(
|
61
|
+
Sheet.addCommand('g-', 'hide-uniform-cols', 'sheet.hide_uniform_cols()', 'hide any column that has multiple rows but only one distinct value')
|
57
62
|
|
58
63
|
Sheet.addCommand('gv', 'unhide-cols', 'unhide_cols(columns, visibleRows)', 'unhide all hidden columns on current sheet')
|
59
64
|
Sheet.addCommand('v', 'toggle-multiline', 'for c in visibleCols: c.toggleMultiline()', 'toggle multiline display')
|
visidata/features/melt.py
CHANGED
@@ -0,0 +1,103 @@
|
|
1
|
+
import itertools
|
2
|
+
|
3
|
+
from visidata import Sheet, ListAggregator, SettableColumn
|
4
|
+
from visidata import vd, anytype, asyncthread, Progress
|
5
|
+
|
6
|
+
class RankAggregator(ListAggregator):
|
7
|
+
'''
|
8
|
+
Ranks start at 1, and each group's rank is 1 higher than the previous group.
|
9
|
+
When elements are tied in ranking, each of them gets the same rank.
|
10
|
+
'''
|
11
|
+
def aggregate(self, col, rows) -> [int]:
|
12
|
+
return self.aggregate_list(col, rows)
|
13
|
+
|
14
|
+
def aggregate_list(self, col, rows) -> [int]:
|
15
|
+
if not col.sheet.keyCols:
|
16
|
+
vd.error('ranking requires one or more key columns')
|
17
|
+
return self.rank(col, rows)
|
18
|
+
|
19
|
+
def rank(self, col, rows):
|
20
|
+
if col.keycol:
|
21
|
+
vd.warning('rank aggregator is uninformative for key columns')
|
22
|
+
def _key_progress(prog):
|
23
|
+
def identity(val):
|
24
|
+
prog.addProgress(1)
|
25
|
+
return val
|
26
|
+
return identity
|
27
|
+
with Progress(gerund='ranking', total=4*col.sheet.nRows) as prog:
|
28
|
+
p = _key_progress(prog) # increment progress every time p() is called
|
29
|
+
# compile row data, for each row a list of tuples: (group_key, rank_key, rownum)
|
30
|
+
rowdata = [(col.sheet.rowkey(r), col.getTypedValue(r), p(rownum)) for rownum, r in enumerate(rows)]
|
31
|
+
# sort by row key and column value to prepare for grouping
|
32
|
+
# If the column is in descending order, use descending order for within-group ranking.
|
33
|
+
reverse = next((r for (c, r) in col.sheet.ordering if c == col or c == col.name), False)
|
34
|
+
try:
|
35
|
+
rowdata.sort(reverse=reverse, key=p)
|
36
|
+
if reverse:
|
37
|
+
vd.status('ranking {col.name} in descending order')
|
38
|
+
except TypeError as e:
|
39
|
+
vd.fail(f'elements in a ranking column must be comparable: {e.args[0]}')
|
40
|
+
rowvals = []
|
41
|
+
#group by row key
|
42
|
+
for _, group in itertools.groupby(rowdata, key=lambda v: v[0]):
|
43
|
+
# within a group, the rows have already been sorted by col_val
|
44
|
+
group = list(group)
|
45
|
+
# rank each group individually
|
46
|
+
group_ranks = rank_sorted_iterable([p(col_val) for _, col_val, rownum in group])
|
47
|
+
rowvals += [(rownum, rank) for (_, _, rownum), rank in zip(group, group_ranks)]
|
48
|
+
# sort by unique rownum, to make rank results match the original row order
|
49
|
+
rowvals.sort(key=p)
|
50
|
+
rowvals = [ rank for rownum, rank in rowvals ]
|
51
|
+
return rowvals
|
52
|
+
|
53
|
+
vd.aggregators['rank'] = RankAggregator('rank', anytype, helpstr='list of ranks, when grouping by key columns', listtype=int)
|
54
|
+
|
55
|
+
def rank_sorted_iterable(vals_sorted) -> [int]:
|
56
|
+
'''*vals_sorted* is an iterable whose elements form one or more groups.
|
57
|
+
The iterable must already be sorted.'''
|
58
|
+
|
59
|
+
ranks = []
|
60
|
+
val_groups = itertools.groupby(vals_sorted)
|
61
|
+
for rank, (_, val_group) in enumerate(val_groups, 1):
|
62
|
+
for _ in val_group:
|
63
|
+
ranks.append(rank)
|
64
|
+
return ranks
|
65
|
+
|
66
|
+
@Sheet.api
|
67
|
+
@asyncthread
|
68
|
+
def addcol_sheetrank(sheet, rows):
|
69
|
+
'''
|
70
|
+
Each row is ranked within its sheet. Rows are ordered by the
|
71
|
+
value of their key columns.
|
72
|
+
'''
|
73
|
+
if not sheet.keyCols:
|
74
|
+
vd.error('ranking requires one or more key columns')
|
75
|
+
colname = f'{sheet.name}_sheetrank'
|
76
|
+
c = SettableColumn(name=colname, type=int)
|
77
|
+
sheet.addColumnAtCursor(c)
|
78
|
+
def _key_progress(prog):
|
79
|
+
def identity(val):
|
80
|
+
prog.addProgress(1)
|
81
|
+
return val
|
82
|
+
return identity
|
83
|
+
with Progress(gerund='ranking', total=5*sheet.nRows) as prog:
|
84
|
+
p = _key_progress(prog) # increment progress every time p() is called
|
85
|
+
ordering = [(col, reverse) for (col, reverse) in sheet.ordering if col.keycol]
|
86
|
+
rowkeys = [(sheet.rowkey(r), p(rownum), r) for rownum, r in enumerate(rows)]
|
87
|
+
if ordering:
|
88
|
+
vd.status('using custom ordering for keycol sort')
|
89
|
+
keycols_ordered = [col for (col, reverse) in ordering]
|
90
|
+
keycols_unordered = [keycol for keycol in sheet.keyCols if not keycol in keycols_ordered]
|
91
|
+
ordering += [(keycol, False) for keycol in keycols_unordered]
|
92
|
+
def _sortkey(e): # sort the rows by using the column
|
93
|
+
p(None)
|
94
|
+
return sheet.sortkey(e[2], ordering=ordering)
|
95
|
+
rowkeys.sort(key=_sortkey)
|
96
|
+
else:
|
97
|
+
rowkeys.sort(key=p)
|
98
|
+
ranks = rank_sorted_iterable([p(rowkey) for rowkey, _, _ in rowkeys])
|
99
|
+
row_ranks = sorted(zip((rownum for _, rownum, _ in rowkeys), ranks), key=p)
|
100
|
+
row_ranks = [rank for rownum, rank in row_ranks]
|
101
|
+
c.setValues(sheet.rows, *[p(row_rank) for row_rank in row_ranks])
|
102
|
+
|
103
|
+
Sheet.addCommand('', 'addcol-rank-sheet', 'sheet.addcol_sheetrank(rows)', 'add column with the rank of each row based on its key columns')
|
@@ -1,14 +1,17 @@
|
|
1
1
|
import os
|
2
2
|
import time
|
3
3
|
|
4
|
-
from visidata import vd, BaseSheet, Sheet, asyncthread, Path, ScopedSetattr
|
4
|
+
from visidata import vd, BaseSheet, Sheet, asyncignore, asyncthread, Path, ScopedSetattr
|
5
5
|
|
6
6
|
|
7
7
|
@BaseSheet.api
|
8
|
-
@
|
8
|
+
@asyncignore
|
9
9
|
def reload_every(sheet, seconds:int):
|
10
10
|
while True:
|
11
|
-
|
11
|
+
# continue reloading till vd.remove() runs, like when the sheet is quit
|
12
|
+
if not sheet in vd.sheets:
|
13
|
+
break
|
14
|
+
vd.sync(sheet.reload())
|
12
15
|
time.sleep(seconds)
|
13
16
|
|
14
17
|
|
@@ -20,13 +23,13 @@ def reload_modified(sheet):
|
|
20
23
|
assert isinstance(p, Path)
|
21
24
|
assert not p.is_url()
|
22
25
|
|
23
|
-
mtime =
|
26
|
+
mtime = 0
|
24
27
|
while True:
|
25
|
-
time.sleep(1)
|
26
28
|
t = os.stat(p).st_mtime
|
27
29
|
if t != mtime:
|
28
30
|
mtime = t
|
29
31
|
sheet.reload_rows()
|
32
|
+
time.sleep(1)
|
30
33
|
|
31
34
|
|
32
35
|
@Sheet.api
|
@@ -46,7 +49,7 @@ def reload_rows(self):
|
|
46
49
|
|
47
50
|
BaseSheet.addCommand('', 'reload-every', 'sheet.reload_every(input("reload interval (sec): ", value=1))', 'schedule sheet reload every N seconds') #683
|
48
51
|
BaseSheet.addCommand('', 'reload-modified', 'sheet.reload_modified()', 'reload sheet when source file modified (tail-like behavior)') #1686
|
49
|
-
|
52
|
+
Sheet.addCommand('z^R', 'reload-rows', 'preloadHook(); reload_rows(); status("reloaded")', 'Reload current sheet leaving current columns intact')
|
50
53
|
|
51
54
|
vd.addMenuItems('''
|
52
55
|
File > Reload > rows only > reload-rows
|
visidata/features/sysedit.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
from copy import copy
|
2
|
-
from visidata import vd, asyncthread, Path, Sheet, IndexSheet
|
2
|
+
from visidata import vd, asyncthread, Path, Sheet, IndexSheet, TableSheet
|
3
3
|
|
4
4
|
|
5
5
|
@Sheet.api
|
@@ -37,7 +37,17 @@ def syseditCells_async(sheet, cols, rows, filetype=None):
|
|
37
37
|
tempcol = tempvs.colsByName.get(col.name)
|
38
38
|
if not tempcol: # column not in edited version
|
39
39
|
continue
|
40
|
-
|
41
|
-
|
42
|
-
|
40
|
+
# only assign values that were changed by the editor
|
41
|
+
edited_rows = []
|
42
|
+
edited_vals = []
|
43
|
+
for r, r_edited in zip(rows, tempvs.rows):
|
44
|
+
v = tempcol.getDisplayValue(r_edited)
|
45
|
+
if col.getDisplayValue(r) != v:
|
46
|
+
edited_rows.append(r)
|
47
|
+
edited_vals.append(v)
|
48
|
+
if edited_rows:
|
49
|
+
col.setValuesTyped(edited_rows, *edited_vals)
|
50
|
+
|
51
|
+
|
52
|
+
TableSheet.addCommand('^O', 'sysedit-cell', 'cd = cursorDisplay; e = vd.launchExternalEditor(cd); cursorCol.setValues([cursorRow], e) if e != cd else None', 'edit current cell in external $EDITOR')
|
43
53
|
Sheet.addCommand('g^O', 'sysedit-selected', 'syseditCells(visibleCols, onlySelectedRows)', 'edit rows in $EDITOR')
|
visidata/features/transpose.py
CHANGED
@@ -25,3 +25,4 @@ class TransposeSheet(Sheet):
|
|
25
25
|
Sheet.addCommand('T', 'transpose', 'vd.push(TransposeSheet(name+"_T", source=sheet))', 'open new sheet with rows and columns transposed')
|
26
26
|
|
27
27
|
vd.addMenuItems('Data > Transpose > transpose')
|
28
|
+
vd.addGlobals(TransposeSheet=TransposeSheet)
|
visidata/features/window.py
CHANGED
@@ -36,6 +36,16 @@ class WindowColumn(Column):
|
|
36
36
|
|
37
37
|
return self._windowrows
|
38
38
|
|
39
|
+
def __getstate__(self):
|
40
|
+
r = super().__getstate__()
|
41
|
+
r['before'] = self.before
|
42
|
+
r['after'] = self.after
|
43
|
+
r['sourcecol'] = self.sourcecol.name
|
44
|
+
return r
|
45
|
+
|
46
|
+
def __setstate__(self, r):
|
47
|
+
self.sourcecol = self.sheet.column(r.pop('sourcecol', None))
|
48
|
+
return super().__setstate__(r)
|
39
49
|
|
40
50
|
@Sheet.api
|
41
51
|
def addcol_window(sheet, curcol):
|
@@ -54,3 +64,5 @@ Sheet.addCommand('w', 'addcol-window', 'addcol_window(cursorCol)', 'add column w
|
|
54
64
|
Sheet.addCommand('', 'select-around-n', 'select_around(input("select rows around selected: ", value=1))', 'select additional N rows before/after each selected row')
|
55
65
|
|
56
66
|
vd.addMenuItem('Row', 'Select', 'N rows around each selected row', 'select-around-n')
|
67
|
+
|
68
|
+
vd.addGlobals(WindowColumn=WindowColumn)
|
visidata/form.py
CHANGED
@@ -40,7 +40,7 @@ class FormCanvas(BaseSheet):
|
|
40
40
|
continue
|
41
41
|
x, y = r.x, r.y
|
42
42
|
if isinstance(y, float) and (0 < y < 1) or (-1 < y < 0): y = h*y
|
43
|
-
if isinstance(x, float) and (0 < x < 1) or (-1 < x < 0): x = w*x-(
|
43
|
+
if isinstance(x, float) and (0 < x < 1) or (-1 < x < 0): x = w*x-(dispwidth(r.text)/2)
|
44
44
|
x = int(x)
|
45
45
|
y = int(y)
|
46
46
|
if y < 0: y += h
|
@@ -52,7 +52,7 @@ class FormCanvas(BaseSheet):
|
|
52
52
|
# underline first occurrence of r.key in r.text
|
53
53
|
if hasattr(r, 'key') and r.key:
|
54
54
|
index = r.text.find(r.key)
|
55
|
-
clipdraw(scr, y, x+index, r.text[index:len(r.key)
|
55
|
+
clipdraw(scr, y, x+index, r.text[index:index+len(r.key)], colors[color + " underline"])
|
56
56
|
vd.onMouse(scr, x, y, dispwidth(r.text), 1,
|
57
57
|
BUTTON1_PRESSED=lambda y,x,key,r=r,sheet=self: sheet.onPressed(r),
|
58
58
|
BUTTON1_RELEASED=lambda y,x,key,r=r,sheet=self: sheet.onReleased(r))
|
@@ -61,7 +61,7 @@ class FormCanvas(BaseSheet):
|
|
61
61
|
vd.setWindows(vd.scrFull)
|
62
62
|
drawnrows = [r for r in self.source.rows if r.text]
|
63
63
|
inputs = [r for r in self.source.rows if r.input]
|
64
|
-
maxw = max(int(r.x)+
|
64
|
+
maxw = max(int(r.x)+dispwidth(r.text) for r in drawnrows)
|
65
65
|
maxh = max(int(r.y) for r in drawnrows)
|
66
66
|
h, w = vd.scrFull.getmaxyx()
|
67
67
|
y, x = max(0, (h-maxh)//2-1), max(0, (w-maxw)//2-1)
|
@@ -103,7 +103,7 @@ class FormCanvas(BaseSheet):
|
|
103
103
|
@VisiData.api
|
104
104
|
def confirm(vd, prompt, exc=EscapeException):
|
105
105
|
'Display *prompt* on status line and demand input that starts with "Y" or "y" to proceed. Raise *exc* otherwise. Return True.'
|
106
|
-
if vd.options.batch
|
106
|
+
if vd.options.batch:
|
107
107
|
return vd.fail('cannot confirm in batch mode: ' + prompt)
|
108
108
|
|
109
109
|
form = FormSheet('confirm', rows=[
|
visidata/freqtbl.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
from copy import copy
|
2
2
|
import itertools
|
3
3
|
|
4
|
-
from visidata import vd,
|
4
|
+
from visidata import vd, vlen, VisiData, Column, AttrColumn, Sheet, ColumnsSheet, ENTER, Fanout
|
5
5
|
from visidata.pivot import PivotSheet, PivotGroupRow
|
6
6
|
|
7
7
|
|
@@ -60,13 +60,57 @@ Each row on this sheet corresponds to a *bin* of rows on the source sheet that h
|
|
60
60
|
return '+'.join(c.name for c in self.groupByCols)
|
61
61
|
|
62
62
|
def selectRow(self, row):
|
63
|
-
|
63
|
+
# Does not create an undo-operation for the select on the source rows. The caller should create undo-information itself.
|
64
|
+
self.source.select(row.sourcerows, add_undo=False) # select all entries in the bin on the source sheet
|
64
65
|
return super().selectRow(row) # then select the bin itself on this sheet
|
65
66
|
|
66
67
|
def unselectRow(self, row):
|
67
|
-
self.source.unselect(row.sourcerows)
|
68
|
+
self.source.unselect(row.sourcerows, add_undo=False)
|
68
69
|
return super().unselectRow(row)
|
69
70
|
|
71
|
+
def addUndoSelection(self):
|
72
|
+
self.source.addUndoSelection()
|
73
|
+
super().addUndoSelection()
|
74
|
+
|
75
|
+
# override Sheet operations that handle multiple rows:
|
76
|
+
# select(), unselect(), and toggle()
|
77
|
+
# to make undo more efficient. Without this optimization, the memory
|
78
|
+
# use for the undo-tracking on the source sheet is O(n^2) in the number
|
79
|
+
# of bins selected, which can easily exceed all available memory.
|
80
|
+
def select(self, rows, status=True, progress=True, add_undo=True):
|
81
|
+
if add_undo:
|
82
|
+
self.addUndoSelection()
|
83
|
+
super().select(rows, status, progress, add_undo=False)
|
84
|
+
|
85
|
+
def unselect(self, rows, status=True, progress=True, add_undo=True):
|
86
|
+
if add_undo:
|
87
|
+
self.addUndoSelection()
|
88
|
+
super().unselect(rows, status, progress, add_undo=False)
|
89
|
+
|
90
|
+
def toggle(self, rows, add_undo=True):
|
91
|
+
'Toggle selection of given *rows* and corresponding rows in source sheet.'
|
92
|
+
if add_undo:
|
93
|
+
self.addUndoSelection()
|
94
|
+
super().toggle(rows, add_undo=False)
|
95
|
+
|
96
|
+
def select_row(self, row, add_undo=True):
|
97
|
+
'Add single *row* to set of selected rows, and corresponding rows in source sheet.'
|
98
|
+
if add_undo:
|
99
|
+
self.addUndoSelection()
|
100
|
+
super().select_row(row, add_undo=False)
|
101
|
+
|
102
|
+
def unselect_row(self, row, add_undo=True):
|
103
|
+
'Remove single *row* from set of selected rows, and remove corresponding rows in source sheet.'
|
104
|
+
if add_undo:
|
105
|
+
self.addUndoSelection()
|
106
|
+
super().unselect_row(row, add_undo=False)
|
107
|
+
|
108
|
+
def toggle_row(self, row, add_undo=True):
|
109
|
+
'Toggle selection of given *row* and of corresponding rows in source sheet.'
|
110
|
+
if add_undo:
|
111
|
+
self.addUndoSelection()
|
112
|
+
super().toggle_row(row, add_undo=False)
|
113
|
+
|
70
114
|
def resetCols(self):
|
71
115
|
super().resetCols()
|
72
116
|
|
visidata/fuzzymatch.py
CHANGED
@@ -367,23 +367,26 @@ CombinedMatch = collections.namedtuple('CombinedMatch', 'score formatted match')
|
|
367
367
|
|
368
368
|
@VisiData.api
|
369
369
|
def fuzzymatch(vd, haystack:"list[dict[str, str]]", needles:"list[str]) -> list[CombinedMatch]"):
|
370
|
-
'Return sorted list of matching dict values in haystack, augmenting the input dicts with _score:int and _positions:dict[k,set[int]] where k is each non-_ key in the haystack dict.'
|
371
|
-
|
370
|
+
'''Perform case-insensitive matching. Return sorted list of matching dict values in haystack, augmenting the input dicts with _score:int and _positions:dict[k,set[int]] where k is each non-_ key in the haystack dict.'''
|
371
|
+
needles = [ p.lower() for p in needles]
|
372
372
|
matches = []
|
373
373
|
for h in haystack:
|
374
374
|
match = {}
|
375
375
|
formatted_hay = {}
|
376
376
|
for k, v in h.items():
|
377
377
|
if k[0] == '_': continue
|
378
|
+
positions = set()
|
379
|
+
v = v.lower()
|
378
380
|
for p in needles:
|
379
381
|
mr = _fuzzymatch(v, p)
|
380
382
|
if mr.score > 0:
|
381
|
-
match[
|
382
|
-
|
383
|
+
match.setdefault(k, []).append(mr)
|
384
|
+
positions |= set(mr.positions)
|
385
|
+
formatted_hay[k] = _format_match(v, positions)
|
383
386
|
|
384
387
|
if match:
|
385
388
|
# square to prefer larger scores in a single haystack
|
386
|
-
score = int(sum(mr.score**2 for
|
389
|
+
score = int(sum([mr.score**2 for mrs in match.values() for mr in mrs]))
|
387
390
|
matches.append(CombinedMatch(score=score, formatted=formatted_hay, match=h))
|
388
391
|
|
389
392
|
return sorted(matches, key=lambda m: -m.score)
|