visidata 2.11.1__py3-none-any.whl → 3.0__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 +72 -91
- visidata/_input.py +259 -42
- visidata/_open.py +84 -29
- visidata/_types.py +21 -3
- visidata/_urlcache.py +17 -4
- visidata/aggregators.py +65 -25
- visidata/apps/__init__.py +0 -0
- visidata/apps/vdsql/__about__.py +8 -0
- visidata/apps/vdsql/__init__.py +5 -0
- visidata/apps/vdsql/__main__.py +27 -0
- visidata/apps/vdsql/_ibis.py +748 -0
- visidata/apps/vdsql/bigquery.py +61 -0
- visidata/apps/vdsql/clickhouse.py +53 -0
- visidata/apps/vdsql/setup.py +40 -0
- visidata/apps/vdsql/snowflake.py +67 -0
- visidata/apps/vgit/__init__.py +13 -0
- {vgit → visidata/apps/vgit}/blame.py +5 -2
- {vgit → visidata/apps/vgit}/branch.py +31 -16
- {vgit → visidata/apps/vgit}/config.py +3 -3
- visidata/apps/vgit/diff.py +169 -0
- visidata/apps/vgit/gitsheet.py +161 -0
- {vgit → visidata/apps/vgit}/grep.py +6 -5
- visidata/apps/vgit/log.py +81 -0
- {vgit → visidata/apps/vgit}/main.py +18 -5
- {vgit → visidata/apps/vgit}/remote.py +8 -4
- visidata/apps/vgit/repos.py +71 -0
- {vgit → visidata/apps/vgit}/setup.py +6 -4
- visidata/apps/vgit/stash.py +69 -0
- visidata/apps/vgit/status.py +204 -0
- {vgit → visidata/apps/vgit}/statusbar.py +2 -0
- visidata/basesheet.py +59 -50
- visidata/canvas.py +208 -93
- visidata/choose.py +6 -6
- visidata/clean_names.py +29 -0
- visidata/clipboard.py +73 -17
- visidata/cliptext.py +220 -46
- visidata/cmdlog.py +88 -114
- visidata/color.py +142 -56
- visidata/column.py +121 -129
- visidata/ddw/input.ddw +74 -79
- visidata/ddw/regex.ddw +57 -0
- visidata/ddwplay.py +33 -14
- visidata/deprecated.py +77 -3
- visidata/desktop/visidata.desktop +7 -0
- visidata/editor.py +12 -6
- visidata/errors.py +5 -1
- visidata/experimental/__init__.py +0 -0
- visidata/experimental/diff_sheet.py +29 -0
- visidata/experimental/digit_autoedit.py +6 -0
- visidata/experimental/gdrive.py +89 -0
- visidata/experimental/google.py +37 -0
- visidata/experimental/gsheets.py +79 -0
- visidata/experimental/live_search.py +37 -0
- visidata/experimental/liveupdate.py +45 -0
- visidata/experimental/mark.py +133 -0
- visidata/experimental/noahs_tapestry/__init__.py +1 -0
- visidata/experimental/noahs_tapestry/tapestry.py +147 -0
- visidata/experimental/rownum.py +73 -0
- visidata/experimental/slide_cells.py +26 -0
- visidata/expr.py +8 -4
- visidata/extensible.py +30 -5
- visidata/features/__init__.py +0 -0
- visidata/features/addcol_audiometadata.py +42 -0
- visidata/features/addcol_histogram.py +34 -0
- visidata/features/canvas_save_svg.py +69 -0
- visidata/features/change_precision.py +46 -0
- visidata/features/cmdpalette.py +163 -0
- visidata/features/colorbrewer.py +363 -0
- visidata/{colorsheet.py → features/colorsheet.py} +17 -16
- visidata/features/command_server.py +105 -0
- visidata/features/currency_to_usd.py +70 -0
- visidata/{customdate.py → features/customdate.py} +2 -0
- visidata/features/dedupe.py +132 -0
- visidata/{describe.py → features/describe.py} +17 -15
- visidata/features/errors_guide.py +26 -0
- visidata/features/expand_cols.py +202 -0
- visidata/{fill.py → features/fill.py} +3 -1
- visidata/{freeze.py → features/freeze.py} +11 -6
- visidata/features/graph_seaborn.py +79 -0
- visidata/features/helloworld.py +10 -0
- visidata/features/hint_types.py +17 -0
- visidata/{incr.py → features/incr.py} +5 -0
- visidata/{join.py → features/join.py} +107 -53
- visidata/features/known_cols.py +21 -0
- visidata/features/layout.py +62 -0
- visidata/{melt.py → features/melt.py} +32 -21
- visidata/features/normcol.py +118 -0
- visidata/features/open_config.py +7 -0
- visidata/features/open_syspaste.py +18 -0
- visidata/features/ping.py +157 -0
- visidata/features/procmgr.py +208 -0
- visidata/features/random_sample.py +6 -0
- visidata/{regex.py → features/regex.py} +47 -31
- visidata/features/reload_every.py +55 -0
- visidata/features/rename_col_cascade.py +30 -0
- visidata/features/scroll_context.py +60 -0
- visidata/features/select_equal_selected.py +11 -0
- visidata/features/setcol_fake.py +65 -0
- visidata/{slide.py → features/slide.py} +75 -21
- visidata/features/sparkline.py +48 -0
- visidata/features/status_source.py +20 -0
- visidata/{sysedit.py → features/sysedit.py} +2 -1
- visidata/features/sysopen_mailcap.py +46 -0
- visidata/features/term_extras.py +13 -0
- visidata/{transpose.py → features/transpose.py} +5 -4
- visidata/features/type_ipaddr.py +73 -0
- visidata/features/type_url.py +11 -0
- visidata/{unfurl.py → features/unfurl.py} +9 -9
- visidata/{window.py → features/window.py} +2 -2
- visidata/form.py +50 -21
- visidata/freqtbl.py +81 -33
- visidata/fuzzymatch.py +414 -0
- visidata/graph.py +105 -33
- visidata/guide.py +180 -0
- visidata/help.py +75 -44
- visidata/hint.py +39 -0
- visidata/indexsheet.py +109 -0
- visidata/input_history.py +55 -0
- visidata/interface.py +58 -0
- visidata/keys.py +17 -16
- visidata/loaders/__init__.py +9 -0
- visidata/loaders/_pandas.py +61 -21
- visidata/loaders/api_airtable.py +70 -0
- visidata/loaders/api_bitio.py +102 -0
- visidata/loaders/api_matrix.py +148 -0
- visidata/loaders/api_reddit.py +306 -0
- visidata/loaders/api_zulip.py +249 -0
- visidata/loaders/archive.py +41 -7
- visidata/loaders/arrow.py +7 -7
- visidata/loaders/conll.py +49 -0
- visidata/loaders/csv.py +25 -7
- visidata/loaders/eml.py +3 -4
- visidata/loaders/f5log.py +1204 -0
- visidata/loaders/fec.py +325 -0
- visidata/loaders/fixed_width.py +2 -4
- visidata/loaders/frictionless.py +3 -3
- visidata/loaders/geojson.py +8 -5
- visidata/loaders/google.py +48 -0
- visidata/loaders/graphviz.py +4 -4
- visidata/loaders/hdf5.py +4 -4
- visidata/loaders/html.py +48 -10
- visidata/loaders/http.py +84 -30
- visidata/loaders/imap.py +20 -10
- visidata/loaders/jrnl.py +52 -0
- visidata/loaders/json.py +83 -29
- visidata/loaders/jsonla.py +74 -0
- visidata/loaders/lsv.py +15 -11
- visidata/loaders/mailbox.py +40 -0
- visidata/loaders/markdown.py +1 -3
- visidata/loaders/mbtiles.py +4 -5
- visidata/loaders/mysql.py +11 -13
- visidata/loaders/npy.py +7 -7
- visidata/loaders/odf.py +4 -1
- visidata/loaders/orgmode.py +428 -0
- visidata/loaders/pandas_freqtbl.py +14 -20
- visidata/loaders/parquet.py +62 -6
- visidata/loaders/pcap.py +3 -3
- visidata/loaders/pdf.py +4 -3
- visidata/loaders/png.py +19 -13
- visidata/loaders/postgres.py +9 -8
- visidata/loaders/rec.py +7 -3
- visidata/loaders/s3.py +342 -0
- visidata/loaders/sas.py +5 -5
- visidata/loaders/scrape.py +186 -0
- visidata/loaders/shp.py +6 -5
- visidata/loaders/spss.py +5 -6
- visidata/loaders/sqlite.py +68 -28
- visidata/loaders/texttables.py +1 -1
- visidata/loaders/toml.py +60 -0
- visidata/loaders/tsv.py +61 -19
- visidata/loaders/ttf.py +19 -7
- visidata/loaders/unzip_http.py +6 -5
- visidata/loaders/usv.py +1 -1
- visidata/loaders/vcf.py +16 -16
- visidata/loaders/vds.py +10 -7
- visidata/loaders/vdx.py +30 -5
- visidata/loaders/xlsb.py +8 -1
- visidata/loaders/xlsx.py +145 -25
- visidata/loaders/xml.py +6 -3
- visidata/loaders/xword.py +4 -4
- visidata/loaders/yaml.py +15 -5
- visidata/macros.py +129 -42
- visidata/main.py +119 -94
- visidata/mainloop.py +101 -155
- visidata/man/parse_options.py +2 -2
- visidata/man/vd.1 +301 -148
- visidata/man/vd.txt +290 -153
- visidata/memory.py +3 -3
- visidata/menu.py +104 -423
- visidata/metasheets.py +59 -141
- visidata/modify.py +78 -23
- visidata/motd.py +3 -3
- visidata/mouse.py +137 -0
- visidata/movement.py +43 -35
- visidata/optionssheet.py +99 -0
- visidata/path.py +113 -32
- visidata/pivot.py +73 -47
- visidata/plugins.py +65 -192
- visidata/pyobj.py +50 -201
- visidata/rename_col.py +20 -0
- visidata/save.py +37 -20
- visidata/search.py +54 -10
- visidata/selection.py +84 -5
- visidata/settings.py +162 -25
- visidata/sheets.py +229 -257
- visidata/shell.py +51 -21
- visidata/sidebar.py +162 -0
- visidata/sort.py +11 -4
- visidata/statusbar.py +113 -104
- visidata/stored_list.py +43 -0
- visidata/stored_prop.py +38 -0
- visidata/tests/conftest.py +3 -3
- visidata/tests/test_cliptext.py +39 -0
- visidata/tests/test_commands.py +62 -7
- visidata/tests/test_edittext.py +2 -2
- visidata/tests/test_features.py +17 -0
- visidata/tests/test_menu.py +14 -0
- visidata/tests/test_path.py +13 -4
- visidata/text_source.py +53 -0
- visidata/textsheet.py +10 -3
- visidata/theme.py +44 -0
- visidata/themes/__init__.py +0 -0
- visidata/themes/ascii8.py +84 -0
- visidata/themes/asciimono.py +84 -0
- visidata/themes/light.py +17 -0
- visidata/threads.py +87 -39
- visidata/tuiwin.py +22 -0
- visidata/type_currency.py +22 -3
- visidata/type_date.py +31 -9
- visidata/type_floatsi.py +5 -1
- visidata/undo.py +17 -5
- visidata/utils.py +106 -23
- visidata/vdobj.py +28 -17
- visidata/windows.py +10 -0
- visidata/wrappers.py +9 -3
- visidata-3.0.data/data/share/applications/visidata.desktop +7 -0
- {visidata-2.11.1.data → visidata-3.0.data}/data/share/man/man1/vd.1 +301 -148
- {visidata-2.11.1.data → visidata-3.0.data}/data/share/man/man1/visidata.1 +301 -148
- visidata-3.0.data/scripts/vd2to3.vdx +9 -0
- {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/METADATA +12 -8
- visidata-3.0.dist-info/RECORD +257 -0
- {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/WHEEL +1 -1
- vgit/__init__.py +0 -1
- vgit/gitsheet.py +0 -164
- visidata/layout.py +0 -44
- visidata/misc.py +0 -5
- visidata-2.11.1.data/scripts/vgit +0 -9
- visidata-2.11.1.dist-info/RECORD +0 -155
- {vgit → visidata/apps/vgit}/__main__.py +0 -0
- {vgit → visidata/apps/vgit}/abort.py +0 -0
- /visidata/{repeat.py → features/repeat.py} +0 -0
- {visidata-2.11.1.data → visidata-3.0.data}/scripts/vd +0 -0
- {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/LICENSE.gpl3 +0 -0
- {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/entry_points.txt +0 -0
- {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,105 @@
|
|
1
|
+
import io
|
2
|
+
import json
|
3
|
+
import socket
|
4
|
+
from unittest.mock import Mock
|
5
|
+
|
6
|
+
from visidata import vd, VisiData, asyncthread, asyncignore, CommandLogRow, Sheet
|
7
|
+
|
8
|
+
|
9
|
+
vd.option('server_addr', '127.0.0.1', 'IP address to listen for commands', sheettype=None, replay=False)
|
10
|
+
vd.option('server_port', 0, 'port to listen for commands', sheettype=None, replay=False)
|
11
|
+
|
12
|
+
|
13
|
+
class SocketIO(io.RawIOBase):
|
14
|
+
def __init__(self, sock):
|
15
|
+
self.sock = sock
|
16
|
+
|
17
|
+
def read(self, sz=-1):
|
18
|
+
if (sz == -1): sz=0x7FFFFFFF
|
19
|
+
return self.sock.recv(sz)
|
20
|
+
|
21
|
+
def seekable(self):
|
22
|
+
return False
|
23
|
+
|
24
|
+
|
25
|
+
@VisiData.before
|
26
|
+
def mainloop(vd, scr):
|
27
|
+
port = vd.options.server_port
|
28
|
+
if port:
|
29
|
+
vd.command_listener(vd.options.server_addr, port)
|
30
|
+
|
31
|
+
|
32
|
+
@VisiData.api
|
33
|
+
@asyncignore
|
34
|
+
def command_listener(vd, addr, port):
|
35
|
+
while True:
|
36
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
37
|
+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
38
|
+
s.bind((addr, port))
|
39
|
+
s.listen(1)
|
40
|
+
|
41
|
+
conn, (addr, inport) = s.accept()
|
42
|
+
vd.status(f'Connection from {addr}:{inport}')
|
43
|
+
vd.queueCommand('no-op') # update screen
|
44
|
+
vd.command_server(conn)
|
45
|
+
|
46
|
+
|
47
|
+
@VisiData.api
|
48
|
+
@asyncignore
|
49
|
+
def command_server(vd, conn):
|
50
|
+
for line in SocketIO(conn):
|
51
|
+
line = line.decode().strip()
|
52
|
+
if line == 'draw':
|
53
|
+
r = '\n'.join(json.dumps(d) for d in vd.sheet.capture_draw_object())
|
54
|
+
conn.send(r.encode('utf-8')+b'\n')
|
55
|
+
elif line.startswith('{'):
|
56
|
+
cmd = json.loads(line)
|
57
|
+
vd.queueCommand(**cmd)
|
58
|
+
else:
|
59
|
+
longname, *rest = line.split(' ', maxsplit=1)
|
60
|
+
cmd = dict(longname=longname, input=rest[0] if rest else '')
|
61
|
+
vd.queueCommand(**cmd)
|
62
|
+
|
63
|
+
conn.close()
|
64
|
+
|
65
|
+
|
66
|
+
@Sheet.api
|
67
|
+
def capture_draw_object(sheet, topRowIndex=0, nScreenRows=25):
|
68
|
+
'capture interface at the object level'
|
69
|
+
isNull = sheet.isNullFunc()
|
70
|
+
sortkeys = {col:rev for col, rev in sheet._ordering}
|
71
|
+
rows = sheet.rows[topRowIndex:min(topRowIndex+nScreenRows+1, sheet.nRows)]
|
72
|
+
|
73
|
+
for vcolidx, col in enumerate(sheet.visibleCols):
|
74
|
+
colstate = col.__getstate__()
|
75
|
+
|
76
|
+
if col in sortkeys:
|
77
|
+
colstate['sort'] = 'desc' if sortkeys.get(col) else 'asc'
|
78
|
+
|
79
|
+
yield dict(i=vcolidx, _type='column', **colstate)
|
80
|
+
|
81
|
+
for rowidx, row in enumerate(rows):
|
82
|
+
rowstate = dict()
|
83
|
+
|
84
|
+
for notefunc in vd.rowNoters:
|
85
|
+
ch = notefunc(sheet, row)
|
86
|
+
if ch:
|
87
|
+
rowstate['note'] = rowstate.get('note', '') + ch
|
88
|
+
|
89
|
+
for vcolidx, col in enumerate(sheet.visibleCols):
|
90
|
+
cellval = col.getCell(row)
|
91
|
+
|
92
|
+
disp = ''.join(x for _, x in col.display(cellval))
|
93
|
+
cellstate = dict(display=disp)
|
94
|
+
notes = getattr(cellval, 'notes', '')
|
95
|
+
try:
|
96
|
+
if isNull and isNull(cellval.value):
|
97
|
+
notes += sheet.options.disp_note_none
|
98
|
+
except (TypeError, ValueError):
|
99
|
+
pass
|
100
|
+
|
101
|
+
if notes:
|
102
|
+
cellstate['notes'] = notes
|
103
|
+
rowstate[str(vcolidx)] = cellstate
|
104
|
+
|
105
|
+
yield dict(_type='row', i=rowidx, **rowstate)
|
@@ -0,0 +1,70 @@
|
|
1
|
+
'''Provide USD(s) function to convert string like '£300' or '205 AUD' to equivalent US$ as float.
|
2
|
+
Uses data from api.apilayer.com/fixer. Requires an API key for apilayer.com.
|
3
|
+
'''
|
4
|
+
|
5
|
+
from visidata import vd
|
6
|
+
import functools
|
7
|
+
import json
|
8
|
+
|
9
|
+
vd.option('fixer_api_key', '', 'API Key for api.apilayer.com/fixer')
|
10
|
+
vd.option('fixer_cache_days', 1, 'Cache days for currency conversions')
|
11
|
+
|
12
|
+
currency_symbols = {
|
13
|
+
'$': 'USD',
|
14
|
+
'£': 'GBP',
|
15
|
+
'₩': 'KRW',
|
16
|
+
'€': 'EUR',
|
17
|
+
'₪': 'ILS',
|
18
|
+
'zł': 'PLN',
|
19
|
+
'₽': 'RUB',
|
20
|
+
'₫': 'VND',
|
21
|
+
}
|
22
|
+
|
23
|
+
def currency_rates_json(date='latest', base='USD'):
|
24
|
+
url = 'https://api.apilayer.com/fixer/%s?base=%s' % (date, base)
|
25
|
+
return vd.urlcache(
|
26
|
+
url,
|
27
|
+
days=vd.options.fixer_cache_days,
|
28
|
+
headers={
|
29
|
+
# First need to set some additional headers as otherwise apilayers will block it with a 403
|
30
|
+
# See also https://stackoverflow.com/questions/13303449/urllib2-httperror-http-error-403-forbidden
|
31
|
+
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11',
|
32
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
33
|
+
'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.3',
|
34
|
+
'Accept-Encoding': 'none',
|
35
|
+
'Accept-Language': 'en-US,en;q=0.8',
|
36
|
+
'Connection': 'keep-alive',
|
37
|
+
|
38
|
+
# Finally set Apikey
|
39
|
+
'apikey': vd.options.fixer_api_key
|
40
|
+
}
|
41
|
+
).read_text()
|
42
|
+
|
43
|
+
@functools.lru_cache()
|
44
|
+
def currency_rates():
|
45
|
+
return json.loads(currency_rates_json())['rates']
|
46
|
+
|
47
|
+
@functools.lru_cache()
|
48
|
+
def currency_multiplier(src_currency, dest_currency):
|
49
|
+
'returns equivalent value in USD for an amt of currency_code'
|
50
|
+
if src_currency == 'USD':
|
51
|
+
return 1.0
|
52
|
+
eur_usd_mult = currency_rates()['USD']
|
53
|
+
eur_src_mult = currency_rates()[src_currency]
|
54
|
+
usd_mult = eur_usd_mult/eur_src_mult
|
55
|
+
if dest_currency == 'USD':
|
56
|
+
return usd_mult
|
57
|
+
|
58
|
+
return usd_mult/currency_rates()[dest_currency]
|
59
|
+
|
60
|
+
def USD(s):
|
61
|
+
for currency_symbol, currency_code in currency_symbols.items():
|
62
|
+
if currency_symbol in s:
|
63
|
+
amt = float(s.replace(currency_symbol, ''))
|
64
|
+
return amt*currency_multiplier(currency_code, 'USD')
|
65
|
+
|
66
|
+
amtstr, currcode = s.split(' ')
|
67
|
+
return float(amtstr) * currency_multiplier(currcode, 'USD')
|
68
|
+
|
69
|
+
|
70
|
+
vd.addGlobals(USD=USD)
|
@@ -21,3 +21,5 @@ def customdate(sheet, fmtstr):
|
|
21
21
|
|
22
22
|
Sheet.addCommand('z@', 'type-customdate', 'fmt=input("date format: ", type="fmtstr"); cursorCol.type=customdate(fmt); cursorCol.fmtstr=fmt', 'set type of current column to custom date format')
|
23
23
|
ColumnsSheet.addCommand('gz@', 'type-customdate-selected', 'fmt=input("date format: ", type="fmtstr"); onlySelectedRows.type=customdate(fmt); onlySelectedRows.fmtstr=fmt', 'set type of selected columns to date')
|
24
|
+
|
25
|
+
vd.addMenuItems('Column > Type as > custom date format > type-customdate')
|
@@ -0,0 +1,132 @@
|
|
1
|
+
"""
|
2
|
+
# Usage
|
3
|
+
|
4
|
+
Duplicates are determined by the sheet's key columns.
|
5
|
+
|
6
|
+
If no key columns are specified, then a duplicate row is one where the values
|
7
|
+
of *all non-hidden* columns are exactly the same as a row that occurs earlier
|
8
|
+
in the sheet.
|
9
|
+
|
10
|
+
If key columns *are* specified, then duplicates are detected based on the
|
11
|
+
values in just those columns.
|
12
|
+
|
13
|
+
## Commands
|
14
|
+
|
15
|
+
- `select-duplicate-rows` sets the selection status in VisiData to `selected`
|
16
|
+
for each row in the active sheet that is a duplicate of a prior row.
|
17
|
+
|
18
|
+
- `dedupe-rows` pushes a new sheet in which only non-duplicate rows in the
|
19
|
+
active sheet are included.
|
20
|
+
"""
|
21
|
+
|
22
|
+
|
23
|
+
__author__ = "Jeremy Singer-Vine <jsvine@gmail.com>"
|
24
|
+
|
25
|
+
from copy import copy
|
26
|
+
|
27
|
+
from visidata import Sheet, BaseSheet, asyncthread, Progress, vd
|
28
|
+
|
29
|
+
|
30
|
+
def gen_identify_duplicates(sheet):
|
31
|
+
"""
|
32
|
+
Takes a sheet, and returns a generator yielding a tuple for each row
|
33
|
+
encountered. The tuple's structure is `(row_object, is_dupe)`, where
|
34
|
+
is_dupe is True/False.
|
35
|
+
|
36
|
+
See note in Usage section above regarding how duplicates are determined.
|
37
|
+
"""
|
38
|
+
|
39
|
+
keyCols = sheet.keyCols
|
40
|
+
|
41
|
+
cols_to_check = None
|
42
|
+
if len(keyCols) == 0:
|
43
|
+
vd.warning("No key cols specified. Using all columns.")
|
44
|
+
cols_to_check = sheet.visibleCols
|
45
|
+
else:
|
46
|
+
cols_to_check = sheet.keyCols
|
47
|
+
|
48
|
+
seen = set()
|
49
|
+
for r in sheet.rows:
|
50
|
+
vals = tuple(col.getValue(r) for col in cols_to_check)
|
51
|
+
is_dupe = vals in seen
|
52
|
+
if not is_dupe:
|
53
|
+
seen.add(vals)
|
54
|
+
yield (r, is_dupe)
|
55
|
+
|
56
|
+
|
57
|
+
@Sheet.api
|
58
|
+
@asyncthread
|
59
|
+
def select_duplicate_rows(sheet, duplicates=True):
|
60
|
+
"""
|
61
|
+
Given a sheet, sets the selection status in VisiData to `selected` for each
|
62
|
+
row that is a duplicate of a prior row.
|
63
|
+
|
64
|
+
If `duplicates = False`, then the behavior is reversed; sets the selection
|
65
|
+
status to `selected` for each row that is *not* a duplicate.
|
66
|
+
"""
|
67
|
+
before = len(sheet.selectedRows)
|
68
|
+
|
69
|
+
gen = gen_identify_duplicates(sheet)
|
70
|
+
prog = Progress(gen, gerund="selecting", total=sheet.nRows)
|
71
|
+
|
72
|
+
for row, is_dupe in prog:
|
73
|
+
if is_dupe == duplicates:
|
74
|
+
sheet.selectRow(row)
|
75
|
+
|
76
|
+
sel_count = len(sheet.selectedRows) - before
|
77
|
+
|
78
|
+
more_str = " more" if before > 0 else ""
|
79
|
+
|
80
|
+
vd.status(f"selected {sel_count}{more_str} {sheet.rowtype}")
|
81
|
+
|
82
|
+
|
83
|
+
@Sheet.api
|
84
|
+
def dedupe_rows(sheet):
|
85
|
+
"""
|
86
|
+
Given a sheet, pushes a new sheet in which only non-duplicate rows are
|
87
|
+
included.
|
88
|
+
"""
|
89
|
+
vs = copy(sheet)
|
90
|
+
vs.name += "_deduped"
|
91
|
+
|
92
|
+
@asyncthread
|
93
|
+
def _reload(self=vs):
|
94
|
+
self.rows = []
|
95
|
+
gen = gen_identify_duplicates(sheet)
|
96
|
+
prog = Progress(gen, gerund="deduplicating", total=sheet.nRows)
|
97
|
+
for row, is_dupe in prog:
|
98
|
+
if not is_dupe:
|
99
|
+
self.addRow(row)
|
100
|
+
|
101
|
+
vs.reload = _reload
|
102
|
+
return vs
|
103
|
+
|
104
|
+
|
105
|
+
# Add longname-commands to VisiData to execute these methods
|
106
|
+
BaseSheet.addCommand(None, "select-duplicate-rows", "sheet.select_duplicate_rows()", "select each row that is a duplicate of a prior row")
|
107
|
+
BaseSheet.addCommand(None, "dedupe-rows", "vd.push(sheet.dedupe_rows())", "open new sheet in which only non-duplicate rows in the active sheet are included")
|
108
|
+
|
109
|
+
vd.addMenuItems('''
|
110
|
+
Row > Select > duplicate rows > select-duplicate-rows
|
111
|
+
Data > Deduplicate rows > dedupe-rows
|
112
|
+
''')
|
113
|
+
|
114
|
+
"""
|
115
|
+
# Changelog
|
116
|
+
|
117
|
+
## 0.2.0 - 2021-09-22
|
118
|
+
|
119
|
+
Use `vd.warning(...)` instead of `warning(...)`
|
120
|
+
|
121
|
+
## 0.1.0 - 2020-10-09
|
122
|
+
|
123
|
+
Revised for compatibility with VisiData 2.x
|
124
|
+
|
125
|
+
## 0.0.1 - 2019-01-01
|
126
|
+
|
127
|
+
Internal change, no external effects: Migrates from ._selectedRows to .selectedRows.
|
128
|
+
|
129
|
+
## 0.0.0 - 2018-12-30
|
130
|
+
|
131
|
+
Initial release.
|
132
|
+
"""
|
@@ -2,12 +2,10 @@ 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, SheetsSheet
|
6
6
|
|
7
|
-
__all__ = ['DescribeSheet']
|
8
7
|
|
9
|
-
|
10
|
-
vd.option('describe_aggrs', 'mean stdev', 'numeric aggregators to calculate on Describe sheet')
|
8
|
+
vd.option('describe_aggrs', 'mean stdev', 'numeric aggregators to calculate on Describe sheet', help=vd.help_aggregators)
|
11
9
|
|
12
10
|
|
13
11
|
@Column.api
|
@@ -24,12 +22,19 @@ def isError(col, row):
|
|
24
22
|
|
25
23
|
class DescribeColumn(Column):
|
26
24
|
def __init__(self, name, **kwargs):
|
25
|
+
kwargs.setdefault('width', 10)
|
27
26
|
super().__init__(name, getter=lambda col,srccol: col.sheet.describeData[srccol].get(col.expr, ''), expr=name, **kwargs)
|
28
27
|
|
29
28
|
|
30
29
|
# rowdef: Column from source sheet
|
31
30
|
class DescribeSheet(ColumnsSheet):
|
32
31
|
# rowtype = 'columns'
|
32
|
+
guide = '''
|
33
|
+
# Describe Sheet
|
34
|
+
This `Describe Sheet` shows a few basic metrics over data in {sheet.displaySource}, with each column represented by a row.
|
35
|
+
|
36
|
+
For example, row {sheet.cursorRowIndex} describes the _{sheet.cursorRow.name}_ column, showing its minimum value, maximum value, mean, median, and other measures.
|
37
|
+
'''
|
33
38
|
precious = True
|
34
39
|
columns = [
|
35
40
|
ColumnAttr('sheet', 'sheet', width=0),
|
@@ -49,17 +54,11 @@ class DescribeSheet(ColumnsSheet):
|
|
49
54
|
]
|
50
55
|
nKeys = 2
|
51
56
|
|
52
|
-
|
53
|
-
|
54
|
-
super().reload()
|
57
|
+
def loader(self):
|
58
|
+
super().loader()
|
55
59
|
self.rows = [c for c in self.rows if not c.hidden]
|
56
60
|
self.describeData = { col: {} for col in self.rows }
|
57
|
-
|
58
|
-
self.columns = []
|
59
|
-
for c in type(self).columns:
|
60
|
-
self.addColumn(c)
|
61
|
-
|
62
|
-
self.setKeys(self.columns[:self.nKeys])
|
61
|
+
self.resetCols()
|
63
62
|
|
64
63
|
for aggrname in vd.options.describe_aggrs.split():
|
65
64
|
self.addColumn(DescribeColumn(aggrname, type=float))
|
@@ -94,8 +93,8 @@ class DescribeSheet(ColumnsSheet):
|
|
94
93
|
for func in [min, max, sum, median]: # use type
|
95
94
|
d[func.__name__] = self.calcStatistic(d, func, vals)
|
96
95
|
for aggrname in vd.options.describe_aggrs.split():
|
97
|
-
|
98
|
-
d[
|
96
|
+
aggr = vd.aggregators[aggrname].funcValues
|
97
|
+
d[aggrname] = self.calcStatistic(d, aggr, vals)
|
99
98
|
|
100
99
|
def calcStatistic(self, d, func, *args, **kwargs):
|
101
100
|
r = wrapply(func, *args, **kwargs)
|
@@ -115,8 +114,11 @@ class DescribeSheet(ColumnsSheet):
|
|
115
114
|
|
116
115
|
TableSheet.addCommand('I', 'describe-sheet', 'vd.push(DescribeSheet(sheet.name+"_describe", source=[sheet]))', 'open Describe Sheet with descriptive statistics for all visible columns')
|
117
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
|
+
SheetsSheet.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')
|
121
121
|
|
122
|
+
vd.addMenuItems('Data > Statistics > describe-sheet')
|
123
|
+
|
122
124
|
vd.addGlobals({'DescribeSheet':DescribeSheet})
|
@@ -0,0 +1,26 @@
|
|
1
|
+
from visidata import GuideSheet, vd
|
2
|
+
|
3
|
+
class ErrorsGuide(GuideSheet):
|
4
|
+
guide_text='''# What was that error?
|
5
|
+
|
6
|
+
Status messages include [:warning]warnings[/] and [:error]errors[/].
|
7
|
+
|
8
|
+
A command may issue a [:warning]warning[/] status and continue running.
|
9
|
+
A command that [:warning]fails[/] or [:error]errors[/] is aborted.
|
10
|
+
|
11
|
+
## Investigating errors further
|
12
|
+
|
13
|
+
If a Python Exception like [:error]RuntimeError[/] appears in the sidebar:
|
14
|
+
|
15
|
+
- {help.commands.error_recent}
|
16
|
+
- {help.commands.errors_all}
|
17
|
+
|
18
|
+
If [:note_type]{vd.options.note_format_exc}[/] or [:error]{vd.options.note_getter_exc}[/] appear inside a cell, it indicates an error happened during calculation, type-conversion, or formatting. When the cursor is on an error cell:
|
19
|
+
|
20
|
+
- {help.commands.error_cell}
|
21
|
+
'''
|
22
|
+
|
23
|
+
|
24
|
+
|
25
|
+
vd.addGuide('ErrorsSheet', ErrorsGuide)
|
26
|
+
|
@@ -0,0 +1,202 @@
|
|
1
|
+
import math
|
2
|
+
import random
|
3
|
+
import os.path
|
4
|
+
from functools import singledispatch
|
5
|
+
|
6
|
+
from visidata import vd, Sheet, asyncthread, Progress, Column, VisiData, deduceType, anytype, getitemdef, ColumnsSheet
|
7
|
+
|
8
|
+
|
9
|
+
@Sheet.api
|
10
|
+
def getSampleRows(sheet):
|
11
|
+
'Return list of sample rows, including the cursor row as the first row.'
|
12
|
+
|
13
|
+
# do not include cursorRow in sample
|
14
|
+
ret = sheet.rows[:sheet.cursorRowIndex] + sheet.rows[sheet.cursorRowIndex+1:]
|
15
|
+
|
16
|
+
n = sheet.options.default_sample_size
|
17
|
+
if n != 0 and n < sheet.nRows:
|
18
|
+
vd.aside(f'sampling {n} rows')
|
19
|
+
ret = random.sample(ret, n)
|
20
|
+
|
21
|
+
return [sheet.cursorRow] + ret
|
22
|
+
|
23
|
+
|
24
|
+
@Sheet.api
|
25
|
+
def expandCols(sheet, cols, rows=None, depth=0):
|
26
|
+
'expand all visible columns of containers to the given depth (0=fully)'
|
27
|
+
ret = []
|
28
|
+
if not rows:
|
29
|
+
rows = sheet.getSampleRows()
|
30
|
+
|
31
|
+
for col in cols:
|
32
|
+
newcols = col.expand(rows)
|
33
|
+
if depth != 1: # countdown not yet complete, or negative (indefinite)
|
34
|
+
ret.extend(sheet.expandCols(newcols, rows, depth-1))
|
35
|
+
return ret
|
36
|
+
|
37
|
+
@singledispatch
|
38
|
+
def _createExpandedColumns(sampleValue, col, rows):
|
39
|
+
'''By default, a column is not expandable. Supported container types for
|
40
|
+
sampleValue trigger alternate, type-specific expansions.'''
|
41
|
+
return []
|
42
|
+
|
43
|
+
@_createExpandedColumns.register(dict)
|
44
|
+
def _(sampleValue, col, vals):
|
45
|
+
'''Build a set of columns to add, using the first occurrence of each key to
|
46
|
+
determine column type'''
|
47
|
+
newcols = {}
|
48
|
+
|
49
|
+
for val in Progress(vals, 'expanding'):
|
50
|
+
if not isinstance(val, dict): # allow mixed-use columns
|
51
|
+
continue
|
52
|
+
colsToAdd = set(val).difference(newcols)
|
53
|
+
colsToAdd and newcols.update({
|
54
|
+
k: deduceType(v)
|
55
|
+
for k, v in val.items()
|
56
|
+
if k in colsToAdd
|
57
|
+
})
|
58
|
+
|
59
|
+
return [
|
60
|
+
ExpandedColumn(col.sheet.options.fmt_expand_dict % (col.name, k), type=v, origCol=col, expr=k)
|
61
|
+
for k, v in newcols.items()
|
62
|
+
]
|
63
|
+
|
64
|
+
def _createExpandedColumnsNamedTuple(col, val):
|
65
|
+
return [
|
66
|
+
ExpandedColumn(col.sheet.options.fmt_expand_dict % (col.name, k), type=colType, origCol=col, expr=i)
|
67
|
+
for i, (k, colType) in enumerate(zip(val._fields, (deduceType(v) for v in val)))
|
68
|
+
]
|
69
|
+
|
70
|
+
@_createExpandedColumns.register(list)
|
71
|
+
@_createExpandedColumns.register(tuple)
|
72
|
+
def _(sampleValue, col, vals):
|
73
|
+
'''Use the longest sequence to determine the number of columns we need to
|
74
|
+
create, and their presumed types. Ignore strings and exceptions. '''
|
75
|
+
def lenNoExceptions(v):
|
76
|
+
try:
|
77
|
+
if isinstance(v, str):
|
78
|
+
return 0
|
79
|
+
return len(v)
|
80
|
+
except Exception as e:
|
81
|
+
return 0
|
82
|
+
|
83
|
+
if hasattr(sampleValue, '_fields'): # looks like a namedtuple
|
84
|
+
return _createExpandedColumnsNamedTuple(col, vals[0])
|
85
|
+
|
86
|
+
longestSeq = max(vals, key=lenNoExceptions)
|
87
|
+
colTypes = [deduceType(v) for v in longestSeq]
|
88
|
+
return [
|
89
|
+
ExpandedColumn(col.sheet.options.fmt_expand_list % (col.name, k), type=colType, origCol=col, expr=k)
|
90
|
+
for k, colType in enumerate(colTypes)
|
91
|
+
]
|
92
|
+
|
93
|
+
|
94
|
+
@Column.api
|
95
|
+
def expand(col, rows):
|
96
|
+
isNull = col.sheet.isNullFunc()
|
97
|
+
nonNulls = [
|
98
|
+
col.getTypedValue(row)
|
99
|
+
for row in rows
|
100
|
+
if not isNull(col.getValue(row))
|
101
|
+
]
|
102
|
+
|
103
|
+
if not nonNulls:
|
104
|
+
return []
|
105
|
+
|
106
|
+
# The type of the first non-null value for col determines if and how the
|
107
|
+
# column can be expanded.
|
108
|
+
expandedCols = _createExpandedColumns(nonNulls[0], col, nonNulls)
|
109
|
+
|
110
|
+
idx = col.sheet.columns.index(col)
|
111
|
+
|
112
|
+
for i, c in enumerate(expandedCols):
|
113
|
+
col.sheet.addColumn(c, index=idx+i+1)
|
114
|
+
if expandedCols:
|
115
|
+
col.hide()
|
116
|
+
return expandedCols
|
117
|
+
|
118
|
+
|
119
|
+
@VisiData.api
|
120
|
+
class ExpandedColumn(Column):
|
121
|
+
def calcValue(self, row):
|
122
|
+
return getitemdef(self.origCol.getValue(row), self.expr)
|
123
|
+
|
124
|
+
def setValue(self, row, value):
|
125
|
+
self.origCol.getValue(row)[self.expr] = value
|
126
|
+
|
127
|
+
|
128
|
+
@Sheet.api
|
129
|
+
@asyncthread
|
130
|
+
def contract_cols(sheet, cols, depth=1): # depth == 0 means contract all the way
|
131
|
+
'Remove any columns in cols with .origCol, and also remove others in sheet.columns which share those .origCol. The inverse of expand.'
|
132
|
+
vd.addUndo(setattr, sheet, 'columns', sheet.columns)
|
133
|
+
for i in range(depth or 10000):
|
134
|
+
colsToClose = [c for c in cols if getattr(c, "origCol", None)]
|
135
|
+
|
136
|
+
if not colsToClose:
|
137
|
+
break
|
138
|
+
|
139
|
+
origCols = set(c.origCol for c in colsToClose)
|
140
|
+
for col in origCols:
|
141
|
+
col.width = sheet.options.default_width
|
142
|
+
|
143
|
+
sheet.columns = [col for col in sheet.columns if getattr(col, 'origCol', None) not in origCols]
|
144
|
+
|
145
|
+
|
146
|
+
@Sheet.api
|
147
|
+
@asyncthread
|
148
|
+
def expand_cols_deep(sheet, cols, rows=None, depth=0): # depth == 0 means drill all the way
|
149
|
+
return sheet.expandCols(cols, rows=rows, depth=depth)
|
150
|
+
|
151
|
+
|
152
|
+
@ColumnsSheet.api
|
153
|
+
def contract_source_cols(sheet, cols):
|
154
|
+
prefix = os.path.commonprefix([c.name for c in cols])
|
155
|
+
ret = ColumnGroup(prefix or 'group', prefix=prefix, sourceCols=cols)
|
156
|
+
for c in cols:
|
157
|
+
c.origCol = ret
|
158
|
+
for vs in sheet.source:
|
159
|
+
vd.addUndo(setattr, vs, 'columns', vs.columns)
|
160
|
+
vs.columns[:] = [c for c in vs.columns if c not in cols]
|
161
|
+
return ret
|
162
|
+
|
163
|
+
|
164
|
+
class ColumnGroup(Column):
|
165
|
+
def calcValue(self, row):
|
166
|
+
return {c.name[len(self.prefix):]:c.getValue(row) for c in self.sourceCols}
|
167
|
+
|
168
|
+
def expand(self, rows):
|
169
|
+
idx = self.sheet.columns.index(self)
|
170
|
+
|
171
|
+
for i, c in enumerate(self.sourceCols):
|
172
|
+
self.sheet.addColumn(c, index=idx+i+1)
|
173
|
+
|
174
|
+
self.hide()
|
175
|
+
|
176
|
+
return self.sourceCols
|
177
|
+
|
178
|
+
|
179
|
+
Sheet.addCommand('(', 'expand-col', 'expand_cols_deep([cursorCol], depth=1)', 'expand current column of containers one level')
|
180
|
+
Sheet.addCommand('g(', 'expand-cols', 'expand_cols_deep(visibleCols, depth=1)', 'expand all visible columns of containers one level')
|
181
|
+
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
|
+
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
|
+
|
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')
|
188
|
+
|
189
|
+
ColumnsSheet.addCommand(')', 'contract-source-cols', 'source[0].addColumn(contract_source_cols(someSelectedRows), index=cursorRowIndex)', 'contract selected columns into column group') #1702
|
190
|
+
|
191
|
+
|
192
|
+
vd.addMenuItems('''
|
193
|
+
Column > Expand > one level > expand-col
|
194
|
+
Column > Expand > to depth N > expand-col-depth
|
195
|
+
Column > Expand > all columns one level > expand-cols
|
196
|
+
Column > Expand > all columns to depth > expand-cols-depth
|
197
|
+
Column > Contract > one level > contract-col
|
198
|
+
Column > Contract > N levels > contract-col-depth
|
199
|
+
Column > Contract > all columns one level > contract-cols
|
200
|
+
Column > Contract > all columns N levels > contract-cols-depth
|
201
|
+
Column > Contract > selected columns on source sheet > contract-source-cols
|
202
|
+
''')
|
@@ -1,4 +1,4 @@
|
|
1
|
-
from visidata import
|
1
|
+
from visidata import vd, VisiData, asyncthread, Sheet, Progress
|
2
2
|
|
3
3
|
|
4
4
|
@VisiData.api
|
@@ -34,3 +34,5 @@ def fillNullValues(vd, col, rows):
|
|
34
34
|
|
35
35
|
|
36
36
|
Sheet.addCommand('f', 'setcol-fill', 'fillNullValues(cursorCol, someSelectedRows)', 'fills null cells in selected rows of current column with contents of non-null cells up the current column')
|
37
|
+
|
38
|
+
vd.addMenuItems('Column > Fill > setcol-fill')
|
@@ -1,5 +1,6 @@
|
|
1
|
-
from visidata import *
|
2
1
|
import collections
|
2
|
+
from visidata import Column, Sheet, VisiData, ColumnItem, Progress, TypedExceptionWrapper, SettableColumn
|
3
|
+
from visidata import asyncthread, vd
|
3
4
|
|
4
5
|
|
5
6
|
@Column.api
|
@@ -35,6 +36,7 @@ class StaticSheet(Sheet):
|
|
35
36
|
def __init__(self, source):
|
36
37
|
super().__init__(source.name + "'", source=source)
|
37
38
|
|
39
|
+
def resetCols(self):
|
38
40
|
self.columns = []
|
39
41
|
for i, col in enumerate(self.source.visibleCols):
|
40
42
|
colcopy = ColumnItem(col.name)
|
@@ -44,12 +46,12 @@ class StaticSheet(Sheet):
|
|
44
46
|
if col in self.source.keyCols:
|
45
47
|
self.setKeys([colcopy])
|
46
48
|
|
47
|
-
|
48
|
-
def reload(self):
|
49
|
-
self.rows = []
|
49
|
+
def iterload(self):
|
50
50
|
for r in Progress(self.source.rows, 'calculating'):
|
51
51
|
row = []
|
52
|
-
|
52
|
+
yield row
|
53
|
+
|
54
|
+
# now fill out row
|
53
55
|
for col in self.source.visibleCols:
|
54
56
|
val = col.getTypedValue(r)
|
55
57
|
if isinstance(val, TypedExceptionWrapper):
|
@@ -58,7 +60,10 @@ class StaticSheet(Sheet):
|
|
58
60
|
row.append(val)
|
59
61
|
|
60
62
|
|
61
|
-
Sheet.addCommand("'", 'freeze-col', 'sheet.addColumnAtCursor(
|
63
|
+
Sheet.addCommand("'", 'freeze-col', 'sheet.addColumnAtCursor(freeze_col(cursorCol))', 'add a frozen copy of current column with all cells evaluated')
|
62
64
|
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')
|
63
65
|
Sheet.addCommand("z'", 'cache-col', 'cursorCol.resetCache()', 'add/reset cache for current column')
|
64
66
|
Sheet.addCommand("gz'", 'cache-cols', 'for c in visibleCols: c.resetCache()', 'add/reset cache for all visible columns')
|
67
|
+
|
68
|
+
vd.addMenuItem('Column', 'Freeze', 'freeze-col')
|
69
|
+
vd.addMenuItem('File', 'Freeze', 'freeze-sheet')
|