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,45 @@
|
|
1
|
+
from visidata import Column, vd, ColumnExpr, CompleteExpr, EscapeException, Sheet
|
2
|
+
|
3
|
+
|
4
|
+
@Column.api
|
5
|
+
def updateExpr(col, val):
|
6
|
+
col.name = val
|
7
|
+
try:
|
8
|
+
col.expr = val
|
9
|
+
except SyntaxError:
|
10
|
+
col.expr = None
|
11
|
+
|
12
|
+
col.sheet.draw(col.sheet._scr)
|
13
|
+
|
14
|
+
|
15
|
+
@Column.api # expr.setter
|
16
|
+
def expr(self, expr):
|
17
|
+
try:
|
18
|
+
self.compiledExpr = compile(expr, '<expr>', 'eval') if expr else None
|
19
|
+
self._expr = expr
|
20
|
+
except SyntaxError as e:
|
21
|
+
self._expr = None
|
22
|
+
|
23
|
+
|
24
|
+
@Sheet.api
|
25
|
+
def addcol_expr(sheet):
|
26
|
+
try:
|
27
|
+
c = sheet.addColumnAtCursor(ColumnExpr("", width=sheet.options.default_width))
|
28
|
+
oldidx = sheet.cursorVisibleColIndex
|
29
|
+
sheet.cursorVisibleColIndex = sheet.visibleCols.index(c)
|
30
|
+
|
31
|
+
expr = sheet.editCell(sheet.cursorVisibleColIndex, -1,
|
32
|
+
completer=CompleteExpr(sheet),
|
33
|
+
updater=lambda val,col=c: col.updateExpr(val))
|
34
|
+
|
35
|
+
c.expr = expr or vd.fail("no expr")
|
36
|
+
c.name = expr
|
37
|
+
c.width = None
|
38
|
+
except (Exception, EscapeException):
|
39
|
+
sheet.columns.remove(c)
|
40
|
+
sheet.cursorVisibleColIndex = oldidx
|
41
|
+
raise
|
42
|
+
|
43
|
+
|
44
|
+
Sheet.addCommand(None, 'addcol-expr', 'sheet.addcol_expr()', "create new column from Python expression, updating the column's calculated values live")
|
45
|
+
Sheet.addCommand(None, 'addcol-new', 'c=addColumnAtIndex(SettableColumn(width=options.default_width)); draw(sheet._scr); cursorVisibleColIndex=visibleCols.index(c); c.name=editCell(cursorVisibleColIndex, -1); c.width=None', 'append new column, updating the column name live')
|
@@ -0,0 +1,133 @@
|
|
1
|
+
'''
|
2
|
+
Marking selected rows with a keystroke, selecting marked rows,
|
3
|
+
and viewing lists of marks and their rows.
|
4
|
+
'''
|
5
|
+
|
6
|
+
from copy import copy
|
7
|
+
from visidata import vd, asyncthread, vlen, VisiData, TableSheet, ColumnItem, RowColorizer
|
8
|
+
|
9
|
+
@VisiData.lazy_property
|
10
|
+
def marks(vd):
|
11
|
+
return MarksSheet('marks')
|
12
|
+
|
13
|
+
|
14
|
+
class MarkSheet(TableSheet):
|
15
|
+
pass
|
16
|
+
|
17
|
+
|
18
|
+
class MarksSheet(TableSheet):
|
19
|
+
'''
|
20
|
+
The Marks Sheet shows all marks in use (on all sheets) and how many rows have each mark.
|
21
|
+
'''
|
22
|
+
rowtype = "marks" # rowdef: [mark, color, [rows]]
|
23
|
+
columns = [
|
24
|
+
ColumnItem('mark', 0),
|
25
|
+
ColumnItem('color', 1),
|
26
|
+
ColumnItem('rows', 2, type=vlen),
|
27
|
+
]
|
28
|
+
colorizers = [
|
29
|
+
RowColorizer(2, None, lambda s,c,r,v: r and r[1])
|
30
|
+
]
|
31
|
+
def __init__(self, *args, **kwargs):
|
32
|
+
super().__init__(*args, **kwargs)
|
33
|
+
self.marknotes = list('0123456789')
|
34
|
+
self.marks = [] #
|
35
|
+
self.markedRows = {} # rowid(row): [row, set(marks)]
|
36
|
+
self.rows = []
|
37
|
+
|
38
|
+
def getColor(self, sheet, row):
|
39
|
+
mark = self.getMark(sheet, row)
|
40
|
+
if not mark:
|
41
|
+
return ''
|
42
|
+
return self.getMarkRow(sheet, mark)[1]
|
43
|
+
|
44
|
+
def getMark(self, sheet, row):
|
45
|
+
mrow = self.markedRows.get(sheet.rowid(row), None)
|
46
|
+
if not mrow:
|
47
|
+
return ''
|
48
|
+
if mrow[1]:
|
49
|
+
return next(iter(mrow[1])) # first item in set
|
50
|
+
|
51
|
+
def getMarks(self, row):
|
52
|
+
'Return set of all marks for given row'
|
53
|
+
return self.markedRows[self.rowid(row)][1]
|
54
|
+
|
55
|
+
def isMarked(self, row, mark):
|
56
|
+
'Return True if given row has given mark'
|
57
|
+
return mark in self.getMarks(row)
|
58
|
+
|
59
|
+
def getMarkRow(self, sheet, mark):
|
60
|
+
for r in self.rows:
|
61
|
+
if r[0] == mark:
|
62
|
+
return r
|
63
|
+
r = [mark, 'color_note_type', MarkSheet('mark_', rows=[], columns=copy(sheet.columns))]
|
64
|
+
self.addRow(r)
|
65
|
+
return r
|
66
|
+
|
67
|
+
def setMark(self, sheet, row, mark):
|
68
|
+
rowid = self.rowid(row)
|
69
|
+
if rowid not in self.markedRows:
|
70
|
+
self.markedRows[rowid] = [row, set(mark)]
|
71
|
+
else:
|
72
|
+
self.markedRows[rowid][1].add(mark)
|
73
|
+
|
74
|
+
vd.marks.getMarkRow(sheet, mark)[2].addRow(row)
|
75
|
+
|
76
|
+
def unsetMark(self, sheet, row, mark):
|
77
|
+
rowid = self.rowid(row)
|
78
|
+
if rowid in self.markedRows:
|
79
|
+
self.markedRows[rowid][1].remove(mark)
|
80
|
+
vd.marks.getMarkRow(sheet, mark)[2].deleteBy(lambda r,x=row: r is x)
|
81
|
+
|
82
|
+
def inputmark(self):
|
83
|
+
return vd.inputsingle('mark: ') or self.marknotes.pop(0)
|
84
|
+
|
85
|
+
def openRow(self, row):
|
86
|
+
return row[2]
|
87
|
+
|
88
|
+
|
89
|
+
@VisiData.api
|
90
|
+
@asyncthread
|
91
|
+
def mark(vd, sheet, rows, m):
|
92
|
+
for r in rows:
|
93
|
+
vd.marks.setMark(sheet, r, m)
|
94
|
+
|
95
|
+
@VisiData.api
|
96
|
+
@asyncthread
|
97
|
+
def unmark(vd, sheet, rows, m):
|
98
|
+
for r in rows:
|
99
|
+
vd.marks.unsetMark(sheet, r, m)
|
100
|
+
|
101
|
+
|
102
|
+
vd.rowNoters.insert(0, lambda sheet, row: vd.marks.getMark(sheet, row))
|
103
|
+
|
104
|
+
TableSheet.colorizers.append(RowColorizer(2, None, lambda s,c,r,v: not c and r and vd.marks.getColor(s, r)))
|
105
|
+
|
106
|
+
TableSheet.addCommand('', 'mark-row', 'vd.mark(sheet, [cursorRow], vd.marks.inputmark())', '')
|
107
|
+
TableSheet.addCommand('', 'unmark-row', 'vd.unmark(sheet, [cursorRow], vd.marks.inputmark())', '')
|
108
|
+
TableSheet.addCommand('', 'mark-selected', 'vd.mark(sheet, selectedRows, vd.marks.inputmark())', '')
|
109
|
+
TableSheet.addCommand('', 'unmark-selected', 'vd.unmark(sheet, selectedRows, vd.marks.inputmark())', '')
|
110
|
+
|
111
|
+
TableSheet.addCommand('', 'select-marks', 'select(gatherBy(lambda r,mark=vd.marks.inputmark(): vd.marks.isMarked(r, mark)), progress=False)', '')
|
112
|
+
TableSheet.addCommand('', 'stoggle-marks', 'toggle(gatherBy(lambda r,mark=vd.marks.inputmark(): vd.marks.isMarked(r, mark)), progress=False)', '')
|
113
|
+
TableSheet.addCommand('', 'unselect-marks', 'unselect(gatherBy(lambda r,mark=vd.marks.inputmark(): vd.marks.isMarked(r, mark)), progress=False)', '')
|
114
|
+
|
115
|
+
TableSheet.addCommand('', 'open-marks', 'vd.push(vd.marks)', '')
|
116
|
+
|
117
|
+
TableSheet.addCommand('', 'go-prev-mark', 'moveToNextRow(lambda row,mark=vd.marks.inputmark(): vd.marks.isMarked(row, mark), reverse=True, msg="no previous marked row")', 'go up current column to previous row with given mark')
|
118
|
+
TableSheet.addCommand('', 'go-next-mark', 'moveToNextRow(lambda row,mark=vd.marks.inputmark(): vd.marks.isMarked(row, mark), msg="no next marked row")', 'go down current column to next row with given mark')
|
119
|
+
|
120
|
+
|
121
|
+
vd.addMenuItems('''
|
122
|
+
View > Marks > open-marks
|
123
|
+
Row > Mark > open Marks Sheet > open-marks
|
124
|
+
Row > Mark > current row > mark-row
|
125
|
+
Row > Mark > selected rows > mark-selected
|
126
|
+
Row > Unmark > current row > unmark-row
|
127
|
+
Row > Unmark > selected rows > unmark-selected
|
128
|
+
Row > Select > marked rows > select-marks
|
129
|
+
Row > Unselect > marked rows > unselect-marks
|
130
|
+
Row > Toggle select > marked rows > stoggle-marks
|
131
|
+
Row > Goto > next marked row > go-next-mark
|
132
|
+
Row > Goto > previous marked row > go-prev-mark
|
133
|
+
''')
|
@@ -0,0 +1 @@
|
|
1
|
+
from . import tapestry
|
@@ -0,0 +1,147 @@
|
|
1
|
+
import json
|
2
|
+
import time
|
3
|
+
import collections
|
4
|
+
import random
|
5
|
+
from functools import cached_property
|
6
|
+
from base64 import b64encode
|
7
|
+
|
8
|
+
from visidata import vd, VisiData, Canvas, Animation, Path, asyncthread, clipdraw, colors, ItemColumn, Sheet, wraptext
|
9
|
+
|
10
|
+
vd.theme_option('color_hint', 'black on yellow', '')
|
11
|
+
|
12
|
+
@VisiData.api
|
13
|
+
def getNoahsPath(vd, name):
|
14
|
+
return Path(vd.pkg_resources_files('visidata')/f'experimental/noahs_tapestry/{name}')
|
15
|
+
|
16
|
+
@VisiData.api
|
17
|
+
def openNoahsText(vd, name):
|
18
|
+
return vd.getNoahsPath(name).open(encoding='utf-8')
|
19
|
+
|
20
|
+
@VisiData.cached_property
|
21
|
+
def noahsDatabase(vd):
|
22
|
+
return vd.open_sqlite(vd.getNoahsPath('noahs.sqlite'))
|
23
|
+
|
24
|
+
class NoahsPuzzle(Sheet):
|
25
|
+
guide = '''
|
26
|
+
# Puzzle {sheet.puznum}
|
27
|
+
- `Shift+A` to input a solution to the puzzle
|
28
|
+
- `Shift+Y` to attempt the current cell as the solution
|
29
|
+
- `Shift+B` to open Noah's Database Backup
|
30
|
+
- `Shift+V` to view Noah's Tapestry
|
31
|
+
'''
|
32
|
+
rowtype = 'lines' # rowdef: [linenum, text]
|
33
|
+
filetype = 'txt'
|
34
|
+
columns = [
|
35
|
+
ItemColumn('linenum', 0, type=int, width=0),
|
36
|
+
ItemColumn('text', 1, width=80, displayer='full'),
|
37
|
+
]
|
38
|
+
precious = False
|
39
|
+
|
40
|
+
def iterload(self):
|
41
|
+
clues = json.loads(vd.getNoahsPath(f'clues.json').read_text())
|
42
|
+
source = vd.getNoahsPath(f'puzzle{self.puznum}.md')
|
43
|
+
winWidth = 78
|
44
|
+
formatted_text = source.open(encoding='utf-8').read().format(**clues)
|
45
|
+
for startingLine, text in enumerate(formatted_text.splitlines()):
|
46
|
+
text = text.strip()
|
47
|
+
if text:
|
48
|
+
for i, (L, _) in enumerate(wraptext(str(text), width=winWidth)):
|
49
|
+
yield [startingLine+i+1, L]
|
50
|
+
else:
|
51
|
+
yield [startingLine+1, text]
|
52
|
+
|
53
|
+
@VisiData.cached_property
|
54
|
+
def noahsSolutions(vd):
|
55
|
+
return json.loads(vd.getNoahsPath(f'solutions.json').read_text())
|
56
|
+
|
57
|
+
@VisiData.api
|
58
|
+
def solve_puzzle(vd, answer):
|
59
|
+
puznum = vd.noahsCurrentPuznum
|
60
|
+
if b64encode(answer.encode()).decode() != vd.noahsSolutions[f'p{puznum}']:
|
61
|
+
vd.fail("Hmmm, that doesn't seem right. Try again?")
|
62
|
+
|
63
|
+
vd.noahsTapestry.solved.add(puznum)
|
64
|
+
vd.status(f'Correct! The candle is now lit.')
|
65
|
+
vd.push(vd.noahsTapestry)
|
66
|
+
|
67
|
+
|
68
|
+
class Tapestry(Canvas):
|
69
|
+
@property
|
70
|
+
def guide(self):
|
71
|
+
ret = ''
|
72
|
+
if vd.screenWidth < 120 or vd.screenHeight < 50:
|
73
|
+
ret = f'''
|
74
|
+
# [:black on yellow]WARNING: TERMINAL TOO SMALL[/]
|
75
|
+
Please expand your terminal to at least 120x50 (currently {vd.screenWidth}x{vd.screenHeight})
|
76
|
+
'''
|
77
|
+
|
78
|
+
ret += '''
|
79
|
+
# Noah's Tapestry
|
80
|
+
An interactive data game
|
81
|
+
|
82
|
+
- `Shift+N` to open the next puzzle
|
83
|
+
'''
|
84
|
+
|
85
|
+
return ret
|
86
|
+
|
87
|
+
solved = set()
|
88
|
+
def reload(self):
|
89
|
+
self.noahs_menorah = Animation(vd.openNoahsText('menorah.ddw'))
|
90
|
+
self.noahs_tapestry = Animation(vd.openNoahsText('tapestry.ddw'))
|
91
|
+
self.noahs_flame = Animation(vd.openNoahsText('flame.ddw'))
|
92
|
+
self.solved = set()
|
93
|
+
|
94
|
+
# self.keep_running()
|
95
|
+
|
96
|
+
@asyncthread
|
97
|
+
def keep_running(self):
|
98
|
+
while True:
|
99
|
+
time.sleep(1)
|
100
|
+
|
101
|
+
def draw(self, scr):
|
102
|
+
solvedays = ['menorah']+[f'day{i}' for i in self.solved]
|
103
|
+
t = time.time()
|
104
|
+
self.noahs_menorah.draw(scr, t=0, y=30, x=19)
|
105
|
+
if not self.solved:
|
106
|
+
clipdraw(scr, 22, 52, "Light the [:italic]shamash[/]", colors['255'])
|
107
|
+
return
|
108
|
+
|
109
|
+
self.noahs_menorah.draw(scr, t=0.1, y=30, x=19, tags=solvedays)
|
110
|
+
self.noahs_tapestry.draw(scr, t=t, tags=solvedays)
|
111
|
+
|
112
|
+
for i in self.solved:
|
113
|
+
xs = [58, 22+9*8, 22+9*7, 22+9*6, 22+9*5, 22+27, 22+18, 22+9, 22, 22]
|
114
|
+
ys = [28, 32, 32, 32, 32, 32, 32, 32, 32, 32]
|
115
|
+
x = xs[i]
|
116
|
+
y = ys[i]
|
117
|
+
|
118
|
+
self.noahs_flame.draw(scr, t=t+(i+random.random())*0.2, y=y, x=x)
|
119
|
+
|
120
|
+
def open_puzzle(self, puznum=None):
|
121
|
+
if puznum is None:
|
122
|
+
puznum = 0
|
123
|
+
if self.solved:
|
124
|
+
puznum = max(self.solved)+1
|
125
|
+
|
126
|
+
vs = NoahsPuzzle('puzzle', str(puznum), source=self, puznum=puznum)
|
127
|
+
vs.ensureLoaded()
|
128
|
+
vd.noahsCurrentPuznum = puznum
|
129
|
+
return vs
|
130
|
+
|
131
|
+
|
132
|
+
@VisiData.lazy_property
|
133
|
+
def noahsTapestry(vd):
|
134
|
+
vd.curses_timeout = 50
|
135
|
+
return Tapestry('noahs', 'tapestry')
|
136
|
+
|
137
|
+
|
138
|
+
NoahsPuzzle.options.color_default = '178 yellow on 232 black'
|
139
|
+
|
140
|
+
vd.addCommand('Shift+B', 'open-noahs-database', 'vd.push(noahsDatabase)', "open database for Noah's Tapestry")
|
141
|
+
vd.addCommand('Shift+V', 'open-noahs-tapestry', 'vd.push(noahsTapestry)', "open Noah's Tapestry")
|
142
|
+
Tapestry.addCommand('Shift+N', 'open-puzzle-next', 'vd.push(open_puzzle())', 'open next unsolved puzzle')
|
143
|
+
NoahsPuzzle.addCommand('Shift+A', 'solve-puzzle-input', 'solve_puzzle(input("Answer: "))', 'input an answer to the current puzzle')
|
144
|
+
Sheet.addCommand('Shift+Y', 'solve-puzzle-cell', 'solve_puzzle(cursorValue)', 'input an answer to the current puzzle')
|
145
|
+
for i in range(9):
|
146
|
+
Tapestry.addCommand(f'{i}', f'open-puzzle-{i}', f'vd.push(open_puzzle({i}))')
|
147
|
+
Tapestry.addCommand(f'Alt+{i}', f'solve-puzzle-force-{i}', f'sheet.solved.add({i})')
|
@@ -0,0 +1,73 @@
|
|
1
|
+
from visidata import *
|
2
|
+
from functools import wraps, partial
|
3
|
+
|
4
|
+
|
5
|
+
@asyncthread
|
6
|
+
@Sheet.api
|
7
|
+
def calcRowIndex(sheet, indexes):
|
8
|
+
for rownum, r in enumerate(sheet.rows):
|
9
|
+
indexes[sheet.rowid(r)] = rownum
|
10
|
+
|
11
|
+
|
12
|
+
@Sheet.lazy_property
|
13
|
+
def _rowindex(sheet):
|
14
|
+
ret = {}
|
15
|
+
sheet.calcRowIndex(ret)
|
16
|
+
return ret
|
17
|
+
|
18
|
+
|
19
|
+
@Sheet.api
|
20
|
+
def rowindex(sheet, row):
|
21
|
+
'Returns the rowindex given the row. May spawn a thread to compute underlying _rowindex.'
|
22
|
+
return sheet._rowindex.get(sheet.rowid(row))
|
23
|
+
|
24
|
+
|
25
|
+
@Sheet.api
|
26
|
+
def prev(sheet, row):
|
27
|
+
'Return the row previous to the given row.'
|
28
|
+
rownum = max(sheet.rowindex(row)-1, 0)
|
29
|
+
return LazyComputeRow(sheet, sheet.rows[rownum])
|
30
|
+
|
31
|
+
|
32
|
+
@Sheet.api
|
33
|
+
def addcol_rowindex(sheet, newcol):
|
34
|
+
oldAddRow = sheet.addRow
|
35
|
+
def rownum_addRow(sheet, col, row, index=None):
|
36
|
+
if index is None:
|
37
|
+
index = len(sheet.rows)
|
38
|
+
|
39
|
+
col._rowindex[sheet.rowid(row)] = index
|
40
|
+
return oldAddRow(row, index)
|
41
|
+
|
42
|
+
# wrapper addRow to keep the index up to date
|
43
|
+
sheet.addRow = wraps(oldAddRow)(partial(rownum_addRow, sheet, newcol))
|
44
|
+
sheet.addColumnAtCursor(newcol)
|
45
|
+
|
46
|
+
# spawn a little thread to calc the rowindex
|
47
|
+
sheet.calcRowIndex(newcol._rowindex)
|
48
|
+
|
49
|
+
|
50
|
+
@Sheet.api
|
51
|
+
def addcol_delta(sheet, vcolidx):
|
52
|
+
col = sheet.visibleCols[vcolidx]
|
53
|
+
|
54
|
+
newcol = ColumnExpr("delta_"+col.name,
|
55
|
+
type=col.type,
|
56
|
+
_rowindex={}, # [rowid(row)] -> rowidx
|
57
|
+
expr="{0}-prev(row).{0}".format(col.name))
|
58
|
+
|
59
|
+
sheet.addcol_rowindex(newcol)
|
60
|
+
return newcol
|
61
|
+
|
62
|
+
@Sheet.api
|
63
|
+
def addcol_rownum(sheet):
|
64
|
+
newcol = Column("rownum",
|
65
|
+
type=int,
|
66
|
+
_rowindex={}, # [rowid(row)] -> rowidx
|
67
|
+
getter=lambda col,row: col._rowindex.get(col.sheet.rowid(row)))
|
68
|
+
|
69
|
+
sheet.addcol_rowindex(newcol)
|
70
|
+
return newcol
|
71
|
+
|
72
|
+
Sheet.addCommand(None, 'addcol-rownum', 'addcol_rownum()', helpstr='add column with original row ordering')
|
73
|
+
Sheet.addCommand(None, 'addcol-delta', 'addcol_delta(cursorVisibleColIndex)', helpstr='add column with delta of current column')
|
@@ -0,0 +1,26 @@
|
|
1
|
+
'''
|
2
|
+
# TODO:
|
3
|
+
- slide-cells-left
|
4
|
+
- slide-cells-<dir>-n
|
5
|
+
- slide-cells-selected-<dir>-n
|
6
|
+
- rename "slide" to "shift"?
|
7
|
+
'''
|
8
|
+
|
9
|
+
from visidata import vd, TableSheet
|
10
|
+
|
11
|
+
|
12
|
+
@TableSheet.api
|
13
|
+
def slide_cells_right(sheet, row, vcolidx):
|
14
|
+
for oldcol, newcol in reversed(list(zip(sheet.visibleCols[vcolidx:], sheet.visibleCols[vcolidx+1:]))):
|
15
|
+
newcol.setValue(row, oldcol.getValue(row))
|
16
|
+
|
17
|
+
sheet.visibleCols[vcolidx].setValue(row, None)
|
18
|
+
|
19
|
+
|
20
|
+
TableSheet.addCommand('', 'slide-cells-right', 'slide_cells_right(cursorRow, cursorVisibleColIndex)', '''
|
21
|
+
Shift individual values in current row one visible column to the right, with leftmost cell set to null.
|
22
|
+
''')
|
23
|
+
|
24
|
+
vd.addMenuItems('''
|
25
|
+
Edit > Slide > cells > right > slide-cells-right
|
26
|
+
''')
|
visidata/expr.py
CHANGED
@@ -38,10 +38,8 @@ def setValuesFromExpr(self, rows, expr):
|
|
38
38
|
for row in Progress(rows, 'setting'):
|
39
39
|
# Note: expressions that are only calculated once, do not need to pass column identity
|
40
40
|
# they can reference their "previous selves" once without causing a recursive problem
|
41
|
-
|
42
|
-
|
43
|
-
except Exception as e:
|
44
|
-
vd.exceptionCaught(e)
|
41
|
+
v = vd.callNoExceptions(self.sheet.evalExpr, compiledExpr, row)
|
42
|
+
vd.callNoExceptions(self.setValue, row, v)
|
45
43
|
self.recalc()
|
46
44
|
vd.status('set %d values = %s' % (len(rows), expr))
|
47
45
|
|
@@ -59,3 +57,9 @@ Sheet.addCommand('gz=', 'setcol-iter', 'cursorCol.setValues(someSelectedRows, *l
|
|
59
57
|
Sheet.addCommand(None, 'show-expr', 'status(evalExpr(inputExpr("show expr="), cursorRow))', 'evaluate Python expression on current row and show result on status line')
|
60
58
|
|
61
59
|
vd.addGlobals({'CompleteExpr': CompleteExpr})
|
60
|
+
|
61
|
+
vd.addMenuItems('''
|
62
|
+
Edit > Modify > current cell > Python expression > setcell-expr
|
63
|
+
Edit > Modify > selected cells > Python sequence > setcol-expr
|
64
|
+
Column > Add column > Python expr > addcol-expr
|
65
|
+
''')
|
visidata/extensible.py
CHANGED
@@ -8,7 +8,7 @@ class Extensible:
|
|
8
8
|
|
9
9
|
@classmethod
|
10
10
|
def init(cls, membername, initfunc=lambda: None, copy=False):
|
11
|
-
'
|
11
|
+
'Prepend equivalent of ``self.<membername> = initfunc()`` to ``<cls>.__init__``. If *copy* is True, <membername> will be copied when object is copied.'
|
12
12
|
|
13
13
|
def thisclass_hasattr(cls, k):
|
14
14
|
return getattr(cls, k, None) is not getattr(cls.__bases__[0], k, None)
|
@@ -16,12 +16,12 @@ class Extensible:
|
|
16
16
|
# must check hasattr first or else this might be parent's __init__
|
17
17
|
oldinit = thisclass_hasattr(cls, '__init__') and getattr(cls, '__init__')
|
18
18
|
def newinit(self, *args, **kwargs):
|
19
|
+
if not hasattr(self, membername): # can be overridden by a subclass
|
20
|
+
setattr(self, membername, initfunc())
|
19
21
|
if oldinit:
|
20
22
|
oldinit(self, *args, **kwargs)
|
21
23
|
else:
|
22
24
|
super(cls, self).__init__(*args, **kwargs)
|
23
|
-
if not hasattr(self, membername): # can be overridden by a subclass
|
24
|
-
setattr(self, membername, initfunc())
|
25
25
|
cls.__init__ = wraps(oldinit)(newinit) if oldinit else newinit
|
26
26
|
|
27
27
|
oldcopy = thisclass_hasattr(cls, '__copy__') and getattr(cls, '__copy__')
|
@@ -30,15 +30,32 @@ class Extensible:
|
|
30
30
|
ret = oldcopy(self, *args, **kwargs)
|
31
31
|
else:
|
32
32
|
ret = super(cls, self).__copy__(*args, **kwargs)
|
33
|
-
|
33
|
+
|
34
|
+
if not hasattr(ret, membername):
|
35
|
+
if copy and hasattr(self, membername):
|
36
|
+
v = getattr(self, membername)
|
37
|
+
else:
|
38
|
+
v = initfunc()
|
39
|
+
setattr(ret, membername, v)
|
40
|
+
|
34
41
|
return ret
|
35
42
|
cls.__copy__ = wraps(oldcopy)(newcopy) if oldcopy else newcopy
|
36
43
|
|
44
|
+
@classmethod
|
45
|
+
def superclasses(cls):
|
46
|
+
yield cls
|
47
|
+
yield from cls.__bases__
|
48
|
+
for b in cls.__bases__:
|
49
|
+
if hasattr(b, 'superclasses'):
|
50
|
+
yield from b.superclasses()
|
51
|
+
|
37
52
|
@classmethod
|
38
53
|
def api(cls, func):
|
39
54
|
oldfunc = getattr(cls, func.__name__, None)
|
40
55
|
if oldfunc:
|
41
56
|
func = wraps(oldfunc)(func)
|
57
|
+
from visidata import vd
|
58
|
+
func.importingModule = vd.importingModule
|
42
59
|
setattr(cls, func.__name__, func)
|
43
60
|
return func
|
44
61
|
|
@@ -75,6 +92,14 @@ class Extensible:
|
|
75
92
|
|
76
93
|
@classmethod
|
77
94
|
def class_api(cls, func):
|
95
|
+
'''`@Class.class_api` works much like `@Class.api`, but for class methods. This method is used internally but may not be all that useful for plugin and module authors. Note that `@classmethod` must still be provided, and **the order of multiple decorators is crucial**, in that `@<class>.class_api` must come before `@classmethod`:
|
96
|
+
|
97
|
+
::
|
98
|
+
@Sheet.class_api
|
99
|
+
@classmethod
|
100
|
+
def addCommand(cls, ...):
|
101
|
+
'''
|
102
|
+
|
78
103
|
name = func.__get__(None, dict).__func__.__name__
|
79
104
|
oldfunc = getattr(cls, name, None)
|
80
105
|
if oldfunc:
|
@@ -99,7 +124,7 @@ class Extensible:
|
|
99
124
|
@property
|
100
125
|
@wraps(func)
|
101
126
|
def get_if_not(self):
|
102
|
-
if getattr(self, name) is None:
|
127
|
+
if getattr(self, name, None) is None:
|
103
128
|
setattr(self, name, func(self))
|
104
129
|
return getattr(self, name)
|
105
130
|
setattr(cls, func.__name__, get_if_not)
|
File without changes
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# requirements: mutagen
|
2
|
+
|
3
|
+
import functools
|
4
|
+
|
5
|
+
from visidata import vd, Column, AttrColumn, DirSheet
|
6
|
+
|
7
|
+
|
8
|
+
@functools.lru_cache(None)
|
9
|
+
def get_mutagen_info(path):
|
10
|
+
mutagen = vd.importExternal('mutagen')
|
11
|
+
m = mutagen.File(path)
|
12
|
+
return m.info
|
13
|
+
|
14
|
+
|
15
|
+
class MutagenColumn(AttrColumn):
|
16
|
+
def calcValue(self, r):
|
17
|
+
md = get_mutagen_info(r)
|
18
|
+
return getattr(md, self.expr, None)
|
19
|
+
|
20
|
+
|
21
|
+
@DirSheet.api
|
22
|
+
def audiometadata_columns(sheet):
|
23
|
+
return [
|
24
|
+
Column('audio_info', width=0, getter=lambda c,r: get_mutagen_info(r)),
|
25
|
+
MutagenColumn('bitrate'),
|
26
|
+
MutagenColumn('channels'),
|
27
|
+
MutagenColumn('encoder_info'),
|
28
|
+
MutagenColumn('encoder_settings'),
|
29
|
+
MutagenColumn('frame_offset'),
|
30
|
+
MutagenColumn('length'),
|
31
|
+
MutagenColumn('mode'),
|
32
|
+
MutagenColumn('padding'),
|
33
|
+
MutagenColumn('protected'),
|
34
|
+
MutagenColumn('sample_rate'),
|
35
|
+
MutagenColumn('track_gain'),
|
36
|
+
]
|
37
|
+
|
38
|
+
|
39
|
+
DirSheet.addCommand('', 'addcol-audiometadata', 'addColumn(*audiometadata_columns())', 'add metadata columns for audio files (MP3, FLAC, Ogg, etc)')
|
40
|
+
|
41
|
+
|
42
|
+
vd.addMenuItems('Column > Add column > audio metadata > addcol-audiometadata')
|
@@ -0,0 +1,34 @@
|
|
1
|
+
from visidata import vd, Column, Sheet, asyncthread, Progress
|
2
|
+
|
3
|
+
|
4
|
+
class HistogramColumn(Column):
|
5
|
+
def calcValue(col, row):
|
6
|
+
histogram = col.sheet.options.disp_histogram
|
7
|
+
histolen = (col.width-2)*col.sourceCol.getTypedValue(row)//(col.sourceCol.largest-col.sourceCol.smallest)
|
8
|
+
return histogram*histolen
|
9
|
+
|
10
|
+
|
11
|
+
@Sheet.api
|
12
|
+
def addcol_histogram(sheet, col): #2052
|
13
|
+
newcol = HistogramColumn(col.name+'_histogram', sourceCol=col)
|
14
|
+
col.smallest = None
|
15
|
+
col.largest = None
|
16
|
+
sheet.calc_histogram_bounds(col)
|
17
|
+
return newcol
|
18
|
+
|
19
|
+
|
20
|
+
@Sheet.api
|
21
|
+
@asyncthread
|
22
|
+
def calc_histogram_bounds(sheet, col):
|
23
|
+
for row in Progress(sheet.rows):
|
24
|
+
v = col.getTypedValue(row)
|
25
|
+
if col.smallest is None or v < col.smallest:
|
26
|
+
col.smallest = v
|
27
|
+
if col.largest is None or v > col.largest:
|
28
|
+
col.largest = v
|
29
|
+
|
30
|
+
|
31
|
+
Sheet.addCommand('', 'addcol-histogram', 'addColumnAtCursor(addcol_histogram(cursorCol))', 'add column with histogram of current column')
|
32
|
+
|
33
|
+
|
34
|
+
vd.addMenuItems('Column > Add column > histogram > addcol-histogram')
|
@@ -0,0 +1,69 @@
|
|
1
|
+
'''
|
2
|
+
Add svg saver to Canvas.
|
3
|
+
|
4
|
+
Requires matplotlib.pyplot
|
5
|
+
'''
|
6
|
+
|
7
|
+
|
8
|
+
import collections
|
9
|
+
|
10
|
+
from visidata import VisiData, Canvas, vd, Progress
|
11
|
+
|
12
|
+
vd.option('plt_marker', '.', 'matplotlib.markers')
|
13
|
+
|
14
|
+
|
15
|
+
@Canvas.api
|
16
|
+
def plot_sheet(self, ax):
|
17
|
+
plt = vd.importExternal('matplotlib.pyplot', 'matplotlib')
|
18
|
+
nerrors = 0
|
19
|
+
nplotted = 0
|
20
|
+
|
21
|
+
self.reset()
|
22
|
+
|
23
|
+
vd.status('loading data points')
|
24
|
+
catcols = [c for c in self.xcols if not vd.isNumeric(c)]
|
25
|
+
numcols = [c for c in self.xcols if vd.isNumeric(c)]
|
26
|
+
for ycol in self.ycols:
|
27
|
+
xpts = collections.defaultdict(list)
|
28
|
+
ypts = collections.defaultdict(list)
|
29
|
+
for rownum, row in enumerate(Progress(self.sourceRows, 'plotting')): # rows being plotted from source
|
30
|
+
try:
|
31
|
+
k = tuple(c.getValue(row) for c in catcols) if catcols else (ycol.name,)
|
32
|
+
|
33
|
+
# convert deliberately to float (to e.g. linearize date)
|
34
|
+
graph_x = numcols[0].type(numcols[0].getValue(row)) if numcols else rownum
|
35
|
+
graph_y = ycol.type(ycol.getValue(row))
|
36
|
+
|
37
|
+
xpts[k].append(graph_x)
|
38
|
+
ypts[k].append(graph_y)
|
39
|
+
|
40
|
+
nplotted += 1
|
41
|
+
except Exception:
|
42
|
+
nerrors += 1
|
43
|
+
if vd.options.debug:
|
44
|
+
raise
|
45
|
+
lines = []
|
46
|
+
for k in xpts:
|
47
|
+
line = ax.scatter(xpts[k], ypts[k], label=' '.join(str(x) for x in k), **vd.options.getall('plt_'))
|
48
|
+
lines.append(line)
|
49
|
+
|
50
|
+
ax.legend(handles=lines)
|
51
|
+
ax.set_xlabel(','.join(xcol.name for xcol in self.xcols if vd.isNumeric(xcol)) or 'row#')
|
52
|
+
ax.xaxis.set_major_locator(plt.MaxNLocator(4))
|
53
|
+
ax.yaxis.set_major_locator(plt.MaxNLocator(4))
|
54
|
+
|
55
|
+
|
56
|
+
@VisiData.api
|
57
|
+
def save_svg(vd, p, *sheets):
|
58
|
+
plt = vd.importExternal('matplotlib.pyplot', 'matplotlib')
|
59
|
+
fig_, ax = plt.subplots()
|
60
|
+
for vs in sheets:
|
61
|
+
if not isinstance(vs, Canvas):
|
62
|
+
vd.warning(f'{vs.name} not a Canvas')
|
63
|
+
continue
|
64
|
+
vs.plot_sheet(ax)
|
65
|
+
|
66
|
+
ax.grid()
|
67
|
+
ax.set_title('\n'.join(vs.name for vs in sheets))
|
68
|
+
plt.xticks()
|
69
|
+
plt.savefig(p, format='svg')
|