visidata 3.1.1__py3-none-any.whl → 3.3__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 +106 -58
- visidata/_open.py +10 -7
- visidata/_types.py +2 -2
- visidata/aggregators.py +125 -16
- visidata/apps/vdsql/_ibis.py +8 -13
- visidata/basesheet.py +4 -3
- visidata/canvas.py +11 -7
- visidata/clipboard.py +11 -2
- visidata/cliptext.py +68 -23
- visidata/cmdlog.py +5 -1
- visidata/column.py +48 -33
- visidata/ddwplay.py +2 -2
- visidata/deprecated.py +96 -63
- visidata/errors.py +41 -5
- visidata/{features → experimental}/helloworld.py +1 -1
- visidata/experimental/liveupdate.py +1 -1
- visidata/expr.py +1 -0
- visidata/extensible.py +4 -0
- visidata/features/cmdpalette.py +64 -25
- visidata/features/describe.py +2 -2
- visidata/features/expand_cols.py +7 -5
- visidata/features/freeze.py +14 -2
- visidata/features/go_col.py +3 -3
- 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 -4
- visidata/features/melt.py +1 -0
- visidata/features/rank.py +103 -0
- visidata/features/reload_every.py +11 -8
- visidata/features/sysedit.py +14 -4
- visidata/features/transpose.py +1 -0
- visidata/features/window.py +12 -0
- visidata/form.py +10 -9
- visidata/freqtbl.py +47 -3
- visidata/fuzzymatch.py +11 -7
- visidata/graph.py +5 -3
- visidata/guides/AggregatorsSheet.md +84 -0
- visidata/guides/CommandsSheet.md +1 -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 +23 -6
- visidata/indexsheet.py +1 -1
- visidata/loaders/_pandas.py +3 -1
- visidata/loaders/archive.py +33 -6
- visidata/loaders/csv.py +12 -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 +5 -4
- visidata/main.py +21 -11
- visidata/mainloop.py +8 -5
- visidata/man/parse_options.py +3 -2
- visidata/man/vd.1 +38 -17
- visidata/man/vd.txt +47 -17
- visidata/menu.py +10 -10
- visidata/metasheets.py +3 -3
- visidata/mouse.py +3 -0
- visidata/movement.py +6 -3
- visidata/pyobj.py +17 -9
- visidata/save.py +10 -2
- visidata/selection.py +29 -18
- visidata/settings.py +9 -5
- visidata/sheets.py +124 -48
- visidata/shell.py +2 -2
- visidata/sidebar.py +11 -8
- visidata/sort.py +89 -11
- visidata/statusbar.py +10 -9
- visidata/tests/test_cliptext.py +164 -0
- visidata/tests/test_commands.py +6 -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 +38 -8
- visidata/utils.py +15 -1
- visidata/vendor/__init__.py +0 -0
- {visidata-3.1.1.data → visidata-3.3.data}/data/share/man/man1/vd.1 +38 -17
- {visidata-3.1.1.data → visidata-3.3.data}/data/share/man/man1/visidata.1 +38 -17
- {visidata-3.1.1.dist-info → visidata-3.3.dist-info}/METADATA +62 -15
- {visidata-3.1.1.dist-info → visidata-3.3.dist-info}/RECORD +98 -92
- {visidata-3.1.1.dist-info → visidata-3.3.dist-info}/WHEEL +1 -1
- {visidata-3.1.1.dist-info → visidata-3.3.dist-info}/entry_points.txt +1 -0
- visidata-3.1.1.data/scripts/vd +0 -6
- {visidata-3.1.1.data → visidata-3.3.data}/data/share/applications/visidata.desktop +0 -0
- {visidata-3.1.1.data → visidata-3.3.data}/scripts/vd2to3.vdx +0 -0
- {visidata-3.1.1.dist-info → visidata-3.3.dist-info}/LICENSE.gpl3 +0 -0
- {visidata-3.1.1.dist-info → visidata-3.3.dist-info}/top_level.txt +0 -0
visidata/features/go_col.py
CHANGED
@@ -25,14 +25,13 @@ def nextColName(sheet, show_cells=True):
|
|
25
25
|
if show_cells and len(sheet.rows) > 0:
|
26
26
|
dv = c.getDisplayValue(sheet.cursorRow)
|
27
27
|
#the underscore that starts _cursor_cell excludes it from being fuzzy matched
|
28
|
-
item = AttrDict(
|
29
|
-
name=c.name,
|
28
|
+
item = AttrDict(name=c.name,
|
30
29
|
_cursor_cell=dv)
|
31
30
|
colnames.append(item)
|
32
31
|
|
33
32
|
def _fmt_colname(match, row, trigger_key):
|
34
33
|
name = match.formatted.get('name', row.name) if match else row.name
|
35
|
-
r = ' '*(
|
34
|
+
r = ' '*(dispwidth(prompt)-3)
|
36
35
|
r += f'[:keystrokes]{trigger_key}[/] '
|
37
36
|
if show_cells and len(sheet.rows) > 0:
|
38
37
|
# pad the right side with spaces
|
@@ -46,6 +45,7 @@ def nextColName(sheet, show_cells=True):
|
|
46
45
|
else:
|
47
46
|
r += name
|
48
47
|
return r
|
48
|
+
|
49
49
|
name = vd.activeSheet.inputPalette(prompt,
|
50
50
|
colnames,
|
51
51
|
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,24 @@ 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()
|
48
52
|
|
49
53
|
Sheet.addCommand('_', 'resize-col-max', 'if cursorCol: cursorCol.toggleWidth(cursorCol.getMaxWidth(visibleRows))', 'toggle width of current column between full and default width')
|
50
54
|
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.
|
55
|
+
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
56
|
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
57
|
|
54
58
|
Sheet.addCommand('-', 'hide-col', 'hide_col(cursorCol)', 'hide the current column')
|
55
|
-
Sheet.addCommand('z-', 'resize-col-half', 'cursorCol.
|
56
|
-
Sheet.addCommand(
|
59
|
+
Sheet.addCommand('z-', 'resize-col-half', 'width = cursorCol.width if cursorCol.width is not None else options.default_width; cursorCol.setWidth(width//2)', 'reduce width of current column by half')
|
60
|
+
Sheet.addCommand('g-', 'hide-uniform-cols', 'sheet.hide_uniform_cols()', 'hide any column that has multiple rows but only one distinct value')
|
57
61
|
|
58
62
|
Sheet.addCommand('gv', 'unhide-cols', 'unhide_cols(columns, visibleRows)', 'unhide all hidden columns on current sheet')
|
59
63
|
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, asyncsingle_queue, 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,19 +23,19 @@ 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
|
33
|
-
@
|
36
|
+
@asyncsingle_queue
|
34
37
|
def reload_rows(self):
|
35
|
-
'Reload rows from ``self.source``, keeping current columns intact. Async.'
|
38
|
+
'''Reload rows from ``self.source``, keeping current columns intact. Async. If previous calls are running, waits for them to finish.'''
|
36
39
|
with (ScopedSetattr(self, 'loading', True),
|
37
40
|
ScopedSetattr(self, 'checkCursor', lambda: True),
|
38
41
|
ScopedSetattr(self, 'cursorRowIndex', self.cursorRowIndex)):
|
@@ -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)
|
@@ -102,8 +102,8 @@ class FormCanvas(BaseSheet):
|
|
102
102
|
@functools.wraps(VisiData.confirm)
|
103
103
|
@VisiData.api
|
104
104
|
def confirm(vd, prompt, exc=EscapeException):
|
105
|
-
'Display *prompt* on status line and demand input that starts with "Y" or "y" to proceed.
|
106
|
-
if vd.options.batch
|
105
|
+
'Display *prompt* on status line and demand input that starts with "Y" or "y" to proceed. Return True when proceeding, otherwise raise *exc*, or if *exc* is falsy, return False'
|
106
|
+
if vd.options.batch:
|
107
107
|
return vd.fail('cannot confirm in batch mode: ' + prompt)
|
108
108
|
|
109
109
|
form = FormSheet('confirm', rows=[
|
@@ -115,10 +115,11 @@ def confirm(vd, prompt, exc=EscapeException):
|
|
115
115
|
])
|
116
116
|
|
117
117
|
ret = FormCanvas(source=form).run(vd.scrFull)
|
118
|
-
if
|
119
|
-
|
120
|
-
|
121
|
-
|
118
|
+
confirmed = False # default is to disconfirm, if user exited the confirmation via: Esc ^C ^Q q
|
119
|
+
if ret:
|
120
|
+
yn = ret['yn'][:1]
|
121
|
+
confirmed = yn and yn in 'Yy'
|
122
|
+
if not confirmed:
|
122
123
|
msg = 'disconfirmed: ' + prompt
|
123
124
|
if exc:
|
124
125
|
raise exc(msg)
|
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, status=False, 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, status=False, 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
@@ -366,24 +366,28 @@ CombinedMatch = collections.namedtuple('CombinedMatch', 'score formatted match')
|
|
366
366
|
|
367
367
|
|
368
368
|
@VisiData.api
|
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
|
-
|
369
|
+
def fuzzymatch(vd, haystack:"list[dict[str, str]]", needles:"list[str]) -> list[CombinedMatch]", case_sensitive=False):
|
370
|
+
'''Perform matching that is case-insensitive by default. 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. Set *case_sensitive* to match case.'''
|
371
|
+
if not case_sensitive:
|
372
|
+
needles = [ p.lower() for p in needles]
|
372
373
|
matches = []
|
373
374
|
for h in haystack:
|
374
375
|
match = {}
|
375
376
|
formatted_hay = {}
|
376
377
|
for k, v in h.items():
|
377
378
|
if k[0] == '_': continue
|
379
|
+
positions = set()
|
380
|
+
v_match = v if case_sensitive else v.lower()
|
378
381
|
for p in needles:
|
379
|
-
mr = _fuzzymatch(
|
382
|
+
mr = _fuzzymatch(v_match, p)
|
380
383
|
if mr.score > 0:
|
381
|
-
match[
|
382
|
-
|
384
|
+
match.setdefault(k, []).append(mr)
|
385
|
+
positions |= set(mr.positions)
|
386
|
+
formatted_hay[k] = _format_match(v, positions)
|
383
387
|
|
384
388
|
if match:
|
385
389
|
# square to prefer larger scores in a single haystack
|
386
|
-
score = int(sum(mr.score**2 for
|
390
|
+
score = int(sum([mr.score**2 for mrs in match.values() for mr in mrs]))
|
387
391
|
matches.append(CombinedMatch(score=score, formatted=formatted_hay, match=h))
|
388
392
|
|
389
393
|
return sorted(matches, key=lambda m: -m.score)
|
visidata/graph.py
CHANGED
@@ -72,6 +72,8 @@ class InvertedCanvas(Canvas):
|
|
72
72
|
|
73
73
|
# provides axis labels, legend
|
74
74
|
class GraphSheet(InvertedCanvas):
|
75
|
+
rowtype = 'points'
|
76
|
+
|
75
77
|
def __init__(self, *names, **kwargs):
|
76
78
|
self.ylabel_maxw = 0
|
77
79
|
super().__init__(*names, **kwargs)
|
@@ -145,7 +147,7 @@ class GraphSheet(InvertedCanvas):
|
|
145
147
|
for char_x in range(0, self.plotwidth//2):
|
146
148
|
has_x_line = char_x in self.reflines_char_x.keys()
|
147
149
|
if has_x_line or has_y_line:
|
148
|
-
cattr = colors.
|
150
|
+
cattr = colors.color_graph_refline
|
149
151
|
if has_x_line:
|
150
152
|
ch = self.reflines_char_x[char_x]
|
151
153
|
# where two lines cross, draw the vertical line, not the horizontal one
|
@@ -268,11 +270,11 @@ class GraphSheet(InvertedCanvas):
|
|
268
270
|
txt = tick + txt
|
269
271
|
else:
|
270
272
|
right_margin = self.plotwidth - 1 - self.plotviewBox.xmax
|
271
|
-
if (
|
273
|
+
if (dispwidth(txt)+dispwidth(tick))*2 <= right_margin:
|
272
274
|
txt = tick + txt
|
273
275
|
else:
|
274
276
|
# shift rightmost label to be left of its tick
|
275
|
-
x -=
|
277
|
+
x -= dispwidth(txt)*2
|
276
278
|
if len(tick) == 0:
|
277
279
|
x += 1
|
278
280
|
txt = txt + tick
|