visidata 3.0.1__py3-none-any.whl → 3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- visidata/__init__.py +12 -10
- visidata/_input.py +208 -199
- visidata/_open.py +4 -1
- visidata/_types.py +4 -3
- visidata/aggregators.py +88 -39
- visidata/apps/vdsql/_ibis.py +9 -11
- visidata/apps/vdsql/clickhouse.py +2 -2
- visidata/apps/vdsql/snowflake.py +1 -1
- visidata/apps/vgit/status.py +1 -1
- visidata/basesheet.py +11 -4
- visidata/canvas.py +66 -24
- visidata/clipboard.py +13 -6
- visidata/cliptext.py +7 -6
- visidata/cmdlog.py +40 -27
- visidata/column.py +14 -49
- visidata/ddw/regex.ddw +3 -2
- visidata/deprecated.py +14 -2
- visidata/desktop/visidata.desktop +2 -2
- visidata/editor.py +1 -0
- visidata/errors.py +1 -1
- visidata/experimental/sort_selected.py +54 -0
- visidata/expr.py +69 -18
- visidata/features/change_precision.py +1 -3
- visidata/features/cmdpalette.py +23 -4
- visidata/features/colorsheet.py +1 -1
- visidata/features/dedupe.py +3 -3
- visidata/features/go_col.py +71 -0
- visidata/features/graph_seaborn.py +1 -1
- visidata/features/join.py +20 -10
- visidata/features/layout.py +18 -5
- visidata/features/ping.py +16 -12
- visidata/features/regex.py +5 -5
- visidata/features/slide.py +15 -17
- visidata/features/status_source.py +3 -1
- visidata/features/sysedit.py +1 -1
- visidata/features/transpose.py +2 -1
- visidata/features/type_ipaddr.py +2 -4
- visidata/features/unfurl.py +1 -0
- visidata/form.py +2 -2
- visidata/freqtbl.py +16 -11
- visidata/fuzzymatch.py +1 -0
- visidata/graph.py +173 -12
- visidata/guide.py +61 -25
- visidata/guides/ClipboardGuide.md +48 -0
- visidata/guides/ColumnsGuide.md +52 -0
- visidata/guides/CommandsSheet.md +28 -0
- visidata/guides/DirSheet.md +34 -0
- visidata/guides/ErrorsSheet.md +17 -0
- visidata/guides/FrequencyTable.md +42 -0
- visidata/guides/GrepSheet.md +28 -0
- visidata/guides/JsonSheet.md +38 -0
- visidata/guides/MacrosSheet.md +19 -0
- visidata/guides/MeltGuide.md +52 -0
- visidata/guides/MemorySheet.md +7 -0
- visidata/guides/MenuGuide.md +26 -0
- visidata/guides/ModifyGuide.md +38 -0
- visidata/guides/PivotGuide.md +71 -0
- visidata/guides/RegexGuide.md +107 -0
- visidata/guides/SelectionGuide.md +44 -0
- visidata/guides/SlideGuide.md +26 -0
- visidata/guides/SortGuide.md +0 -0
- visidata/guides/SplitpaneGuide.md +15 -0
- visidata/guides/TypesSheet.md +43 -0
- visidata/guides/XsvGuide.md +36 -0
- visidata/help.py +6 -6
- visidata/hint.py +2 -1
- visidata/indexsheet.py +2 -2
- visidata/interface.py +13 -14
- visidata/keys.py +4 -1
- visidata/loaders/api_airtable.py +1 -1
- visidata/loaders/archive.py +1 -1
- visidata/loaders/csv.py +9 -5
- visidata/loaders/eml.py +11 -6
- visidata/loaders/f5log.py +1 -0
- visidata/loaders/fec.py +18 -42
- visidata/loaders/fixed_width.py +19 -3
- visidata/loaders/grep.py +121 -0
- visidata/loaders/html.py +1 -0
- visidata/loaders/http.py +6 -1
- visidata/loaders/json.py +22 -4
- visidata/loaders/jsonla.py +8 -2
- visidata/loaders/mailbox.py +1 -0
- visidata/loaders/markdown.py +25 -6
- visidata/loaders/msgpack.py +19 -0
- visidata/loaders/npy.py +0 -1
- visidata/loaders/odf.py +18 -4
- visidata/loaders/orgmode.py +1 -1
- visidata/loaders/rec.py +6 -4
- visidata/loaders/sas.py +11 -4
- visidata/loaders/scrape.py +0 -1
- visidata/loaders/texttables.py +2 -0
- visidata/loaders/tsv.py +24 -7
- visidata/loaders/unzip_http.py +127 -3
- visidata/loaders/vds.py +4 -0
- visidata/loaders/vdx.py +1 -1
- visidata/loaders/xlsx.py +5 -0
- visidata/loaders/xml.py +2 -1
- visidata/macros.py +14 -31
- visidata/main.py +20 -15
- visidata/mainloop.py +17 -6
- visidata/man/vd.1 +74 -39
- visidata/man/vd.txt +73 -41
- visidata/memory.py +16 -5
- visidata/menu.py +14 -3
- visidata/metasheets.py +5 -6
- visidata/modify.py +4 -4
- visidata/mouse.py +2 -0
- visidata/movement.py +14 -28
- visidata/optionssheet.py +3 -5
- visidata/path.py +59 -37
- visidata/pivot.py +8 -5
- visidata/pyobj.py +63 -9
- visidata/rename_col.py +18 -1
- visidata/save.py +16 -9
- visidata/search.py +4 -4
- visidata/selection.py +10 -56
- visidata/settings.py +37 -35
- visidata/sheets.py +189 -118
- visidata/shell.py +23 -14
- visidata/sidebar.py +71 -16
- visidata/sort.py +21 -6
- visidata/statusbar.py +42 -5
- visidata/stored_list.py +5 -2
- visidata/tests/conftest.py +1 -0
- visidata/tests/test_commands.py +9 -1
- visidata/tests/test_completer.py +18 -0
- visidata/tests/test_edittext.py +3 -2
- visidata/text_source.py +7 -4
- visidata/textsheet.py +20 -6
- visidata/themes/ascii8.py +9 -6
- visidata/themes/asciimono.py +14 -4
- visidata/threads.py +13 -3
- visidata/tuiwin.py +5 -1
- visidata/type_currency.py +1 -2
- visidata/type_date.py +6 -1
- visidata/undo.py +10 -13
- visidata/utils.py +9 -3
- visidata/vdobj.py +21 -1
- visidata/wrappers.py +9 -1
- {visidata-3.0.1.data → visidata-3.1.data}/data/share/applications/visidata.desktop +2 -2
- {visidata-3.0.1.data → visidata-3.1.data}/data/share/man/man1/vd.1 +74 -39
- {visidata-3.0.1.data → visidata-3.1.data}/data/share/man/man1/visidata.1 +74 -39
- {visidata-3.0.1.dist-info → visidata-3.1.dist-info}/METADATA +33 -5
- visidata-3.1.dist-info/RECORD +284 -0
- visidata-3.0.1.dist-info/RECORD +0 -258
- {visidata-3.0.1.data → visidata-3.1.data}/scripts/vd +0 -0
- {visidata-3.0.1.data → visidata-3.1.data}/scripts/vd2to3.vdx +0 -0
- {visidata-3.0.1.dist-info → visidata-3.1.dist-info}/LICENSE.gpl3 +0 -0
- {visidata-3.0.1.dist-info → visidata-3.1.dist-info}/WHEEL +0 -0
- {visidata-3.0.1.dist-info → visidata-3.1.dist-info}/entry_points.txt +0 -0
- {visidata-3.0.1.dist-info → visidata-3.1.dist-info}/top_level.txt +0 -0
visidata/graph.py
CHANGED
@@ -1,10 +1,16 @@
|
|
1
1
|
import math
|
2
2
|
|
3
|
-
from visidata import VisiData, Canvas, Sheet, Progress, BoundingBox, Point
|
4
|
-
from visidata import vd, asyncthread, dispwidth, colors, clipstr
|
3
|
+
from visidata import VisiData, Canvas, Sheet, Progress, BoundingBox, Point, ColumnsSheet
|
4
|
+
from visidata import vd, asyncthread, dispwidth, colors, clipstr, ColorAttr, update_attr
|
5
|
+
from visidata.type_date import date
|
6
|
+
from statistics import median
|
5
7
|
|
6
8
|
vd.theme_option('color_graph_axis', 'bold', 'color for graph axis labels')
|
7
9
|
vd.theme_option('disp_graph_tick_x', '╵', 'character for graph x-axis ticks')
|
10
|
+
vd.theme_option('color_graph_refline', '', 'color for graph reference value lines')
|
11
|
+
vd.theme_option('disp_graph_reflines_x_charset', '▏││▕', 'charset to render vertical reference lines on graph')
|
12
|
+
vd.theme_option('disp_graph_reflines_y_charset', '▔──▁', 'charset to render horizontal reference lines on graph')
|
13
|
+
vd.theme_option('disp_graph_multiple_reflines_char', '▒', 'char to render multiple parallel reflines')
|
8
14
|
|
9
15
|
|
10
16
|
@VisiData.api
|
@@ -21,14 +27,18 @@ class InvertedCanvas(Canvas):
|
|
21
27
|
'adjust visibleBox.xymin so that canvasPoint is plotted at plotterPoint'
|
22
28
|
self.visibleBox.xmin = canvasPoint.x - self.canvasW(plotterPoint.x-self.plotviewBox.xmin)
|
23
29
|
self.visibleBox.ymin = canvasPoint.y - self.canvasH(self.plotviewBox.ymax-plotterPoint.y)
|
24
|
-
self.
|
30
|
+
self.resetBounds()
|
31
|
+
|
32
|
+
def rowsWithin(self, plotter_bbox):
|
33
|
+
return super().rowsWithin(plotter_bbox, invert_y=True)
|
25
34
|
|
26
35
|
def zoomTo(self, bbox):
|
27
36
|
super().zoomTo(bbox)
|
28
37
|
self.fixPoint(Point(self.plotviewBox.xmin, self.plotviewBox.ymin),
|
29
|
-
Point(bbox.xmin, bbox.ymax
|
38
|
+
Point(bbox.xmin, bbox.ymax))
|
39
|
+
self.resetBounds()
|
30
40
|
|
31
|
-
def scaleY(self, canvasY):
|
41
|
+
def scaleY(self, canvasY) -> int:
|
32
42
|
'returns a plotter y coordinate for a canvas y coordinate, with the y direction inverted'
|
33
43
|
return self.plotviewBox.ymax-round((canvasY-self.visibleBox.ymin)*self.yScaler)
|
34
44
|
|
@@ -66,6 +76,11 @@ class GraphSheet(InvertedCanvas):
|
|
66
76
|
self.ylabel_maxw = 0
|
67
77
|
super().__init__(*names, **kwargs)
|
68
78
|
|
79
|
+
self.reflines_x = []
|
80
|
+
self.reflines_y = []
|
81
|
+
self.reflines_char_x = {} # { x value in character coordinates -> character to use to draw that vertical line }
|
82
|
+
self.reflines_char_y = {} # { y value in character coordinates -> character to use to draw that horizontal line }
|
83
|
+
|
69
84
|
vd.numericCols(self.xcols) or vd.fail('at least one numeric key col necessary for x-axis')
|
70
85
|
self.ycols or vd.fail('%s is non-numeric' % '/'.join(yc.name for yc in kwargs.get('ycols')))
|
71
86
|
|
@@ -80,6 +95,7 @@ class GraphSheet(InvertedCanvas):
|
|
80
95
|
nplotted = 0
|
81
96
|
|
82
97
|
self.reset()
|
98
|
+
self.row_order = {}
|
83
99
|
|
84
100
|
vd.status('loading data points')
|
85
101
|
catcols = [c for c in self.xcols if not vd.isNumeric(c)]
|
@@ -95,6 +111,7 @@ class GraphSheet(InvertedCanvas):
|
|
95
111
|
|
96
112
|
attr = self.plotColor(k)
|
97
113
|
self.point(graph_x, graph_y, attr, row)
|
114
|
+
self.row_order[self.source.rowid(row)] = rownum
|
98
115
|
nplotted += 1
|
99
116
|
except Exception as e:
|
100
117
|
nerrors += 1
|
@@ -106,11 +123,45 @@ class GraphSheet(InvertedCanvas):
|
|
106
123
|
|
107
124
|
self.xzoomlevel=self.yzoomlevel=1.0
|
108
125
|
self.resetBounds()
|
109
|
-
self.refresh()
|
110
126
|
|
111
|
-
def
|
112
|
-
|
127
|
+
def draw(self, scr):
|
128
|
+
windowHeight, windowWidth = scr.getmaxyx()
|
129
|
+
if self.needsRefresh:
|
130
|
+
self.render(windowHeight, windowWidth)
|
131
|
+
|
132
|
+
# required because we use clear_empty_squares=False for draw_pixels()
|
133
|
+
self.draw_empty(scr)
|
134
|
+
# draw reflines first so pixels draw over them
|
135
|
+
self.draw_reflines(scr)
|
136
|
+
# use clear_empty_squares to keep reflines
|
137
|
+
self.draw_pixels(scr, clear_empty_squares=False)
|
138
|
+
self.draw_labels(scr)
|
139
|
+
|
140
|
+
def draw_reflines(self, scr):
|
141
|
+
cursorBBox = self.plotterCursorBox
|
142
|
+
# draws only on character cells that have reflines, leaves other cells unaffected
|
143
|
+
for char_y in range(0, self.plotheight//4):
|
144
|
+
has_y_line = char_y in self.reflines_char_y.keys()
|
145
|
+
for char_x in range(0, self.plotwidth//2):
|
146
|
+
has_x_line = char_x in self.reflines_char_x.keys()
|
147
|
+
if has_x_line or has_y_line:
|
148
|
+
cattr = colors.color_refline
|
149
|
+
if has_x_line:
|
150
|
+
ch = self.reflines_char_x[char_x]
|
151
|
+
# where two lines cross, draw the vertical line, not the horizontal one
|
152
|
+
elif has_y_line:
|
153
|
+
ch = self.reflines_char_y[char_y]
|
154
|
+
# draw cursor
|
155
|
+
if cursorBBox.contains(char_x*2, char_y*4) or \
|
156
|
+
cursorBBox.contains(char_x*2+1, char_y*4+3):
|
157
|
+
cattr = update_attr(cattr, colors.color_current_row)
|
158
|
+
scr.addstr(char_y, char_x, ch, cattr.attr)
|
159
|
+
|
160
|
+
def resetBounds(self, refresh=True):
|
161
|
+
super().resetBounds(refresh=False)
|
113
162
|
self.createLabels()
|
163
|
+
if refresh:
|
164
|
+
self.refresh()
|
114
165
|
|
115
166
|
def moveToRow(self, rowstr):
|
116
167
|
ymin, ymax = map(float, map(self.parseY, rowstr.split()))
|
@@ -118,6 +169,43 @@ class GraphSheet(InvertedCanvas):
|
|
118
169
|
self.cursorBox.h = ymax-ymin
|
119
170
|
return True
|
120
171
|
|
172
|
+
def plot_elements(self, invert_y=True):
|
173
|
+
self.plot_reflines()
|
174
|
+
super().plot_elements(invert_y=True)
|
175
|
+
|
176
|
+
def plot_reflines(self):
|
177
|
+
self.reflines_char_x = {}
|
178
|
+
self.reflines_char_y = {}
|
179
|
+
|
180
|
+
bb = self.visibleBox
|
181
|
+
xmin, ymin, xmax, ymax = bb.xmin, bb.ymin, bb.xmax, bb.ymax
|
182
|
+
|
183
|
+
for data_y in self.reflines_y:
|
184
|
+
data_y = float(data_y)
|
185
|
+
if data_y >= ymin and data_y <= ymax:
|
186
|
+
char_y, offset = divmod(self.scaleY(data_y), 4)
|
187
|
+
chars = self.options.disp_graph_reflines_y_charset
|
188
|
+
# if we're drawing two different reflines in the same square, fill it with a different char
|
189
|
+
if char_y in self.reflines_char_y and self.reflines_char_y[char_y] != chars[offset]:
|
190
|
+
self.reflines_char_y[char_y] = vd.options.disp_graph_multiple_reflines_char
|
191
|
+
else:
|
192
|
+
self.reflines_char_y[char_y] = chars[offset]
|
193
|
+
|
194
|
+
for data_x in self.reflines_x:
|
195
|
+
data_x = float(data_x)
|
196
|
+
if data_x >= xmin and data_x <= xmax:
|
197
|
+
plot_x = self.scaleX(data_x)
|
198
|
+
# plot_x is an integer count of plotter pixels, and each character box has 2 plotter pixels
|
199
|
+
char_x = plot_x // 2
|
200
|
+
# To subdivide the 2 plotter pixels per square into 4 zones, we have to first multiply by 2.
|
201
|
+
offset = 2*plot_x % 4
|
202
|
+
chars = self.options.disp_graph_reflines_x_charset
|
203
|
+
# if we're drawing two different reflines in the same square, fill it with a different char
|
204
|
+
if char_x in self.reflines_char_x and self.reflines_char_x[char_x] != chars[offset]:
|
205
|
+
self.reflines_char_y[char_x] = vd.options.disp_graph_multiple_reflines_char
|
206
|
+
else:
|
207
|
+
self.reflines_char_x[char_x] = chars[offset]
|
208
|
+
|
121
209
|
def moveToCol(self, colstr):
|
122
210
|
xmin, xmax = map(float, map(self.parseX, colstr.split()))
|
123
211
|
self.cursorBox.xmin = xmin
|
@@ -158,20 +246,23 @@ class GraphSheet(InvertedCanvas):
|
|
158
246
|
return self.ycols[0].type(txt)
|
159
247
|
|
160
248
|
def add_y_axis_label(self, frac):
|
161
|
-
|
249
|
+
label_data_y = self.visibleBox.ymin + frac*self.visibleBox.h
|
250
|
+
txt = self.formatYLabel(label_data_y)
|
162
251
|
w = (dispwidth(txt)+1)*2
|
163
252
|
if self.ylabel_maxw < w:
|
164
253
|
self.ylabel_maxw = w
|
254
|
+
y = self.scaleY(label_data_y)
|
165
255
|
|
166
256
|
# plot y-axis labels on the far left of the canvas, but within the plotview height-wise
|
167
|
-
self.plotlabel(0,
|
257
|
+
self.plotlabel(0, y, txt, 'graph_axis')
|
168
258
|
|
169
259
|
def add_x_axis_label(self, frac):
|
170
|
-
|
260
|
+
label_data_x = self.visibleBox.xmin + frac*self.visibleBox.w
|
261
|
+
txt = self.formatXLabel(label_data_x)
|
171
262
|
tick = vd.options.disp_graph_tick_x or ''
|
172
263
|
|
173
264
|
# plot x-axis labels below the plotviewBox.ymax, but within the plotview width-wise
|
174
|
-
x = self.
|
265
|
+
x = self.scaleX(label_data_x)
|
175
266
|
|
176
267
|
if frac < 1.0:
|
177
268
|
txt = tick + txt
|
@@ -213,9 +304,67 @@ class GraphSheet(InvertedCanvas):
|
|
213
304
|
xname, _ = clipstr(xname, self.left_margin//2-2)
|
214
305
|
self.plotlabel(0, self.plotviewBox.ymax+4, xname+'»', 'graph_axis')
|
215
306
|
|
307
|
+
def rowsWithin(self, plotter_bbox):
|
308
|
+
'return list of deduped rows within plotter_bbox'
|
309
|
+
rows = super().rowsWithin(plotter_bbox)
|
310
|
+
return sorted(rows, key=lambda r: self.row_order[self.source.rowid(r)])
|
311
|
+
|
312
|
+
def draw_refline_x(self):
|
313
|
+
xcol = vd.numericCols(self.xcols)[0]
|
314
|
+
xtype = xcol.type
|
315
|
+
val = median(xcol.getValues(self.sourceRows))
|
316
|
+
suggested = format_input_value(val, xtype)
|
317
|
+
xstrs = vd.input("add line(s) at x = ", type="reflinex", value=suggested, defaultLast=True).split()
|
318
|
+
|
319
|
+
for xstr in xstrs:
|
320
|
+
vals = [ v.strip() for v in xstr.split(',') ]
|
321
|
+
if len(vals) != len(self.xcols):
|
322
|
+
vd.fail(f'must have {len(self.xcols)} x values, had {len(vals)} values: {xstr}')
|
323
|
+
self.reflines_x += [xtype(val) for xcol, val in zip(self.xcols, vals) if xtype(val) not in self.reflines_x ]
|
324
|
+
self.refresh()
|
325
|
+
|
326
|
+
def draw_refline_y(self):
|
327
|
+
ytype = self.ycols[0].type
|
328
|
+
val = median(self.ycols[0].getValues(self.sourceRows))
|
329
|
+
suggested = format_input_value(val, ytype)
|
330
|
+
ystrs = vd.input("add line(s) at y = ", type="refliney", value=suggested, defaultLast=True).split()
|
331
|
+
|
332
|
+
self.reflines_y += [ ytype(y) for y in ystrs if ytype(y) not in self.reflines_y ]
|
333
|
+
self.refresh()
|
334
|
+
|
335
|
+
def erase_refline_x(self):
|
336
|
+
if len(self.reflines_x) == 0:
|
337
|
+
vd.fail(f'no x refline to erase')
|
338
|
+
xtype = vd.numericCols(self.xcols)[0].type
|
339
|
+
suggested = format_input_value(self.reflines_x[0], xtype)
|
340
|
+
|
341
|
+
xstrs = vd.input('remove line(s) at x = ', value=suggested, type='reflinex', defaultLast=True).split()
|
342
|
+
for input_x in xstrs:
|
343
|
+
self.reflines_x.remove(xtype(input_x))
|
344
|
+
self.refresh()
|
345
|
+
|
346
|
+
def erase_refline_y(self):
|
347
|
+
ytype = self.ycols[0].type
|
348
|
+
suggested = format_input_value(self.reflines_y[0], ytype) if self.reflines_y else ''
|
349
|
+
ystrs = vd.input('remove line(s) at y = ', value=suggested, type='refliney', defaultLast=True).split()
|
350
|
+
for y in ystrs:
|
351
|
+
try:
|
352
|
+
self.reflines_y.remove(ytype(y))
|
353
|
+
except ValueError:
|
354
|
+
vd.fail(f'value {y} not in reflines_y')
|
355
|
+
self.refresh()
|
356
|
+
|
357
|
+
def format_input_value(val, type):
|
358
|
+
'''format a value for entry into vd.input(), so its representation has no spaces and no commas'''
|
359
|
+
if type is date:
|
360
|
+
return val.strftime('%Y-%m-%d')
|
361
|
+
else:
|
362
|
+
return str(val)
|
363
|
+
|
216
364
|
|
217
365
|
Sheet.addCommand('.', 'plot-column', 'vd.push(GraphSheet(sheet.name, "graph", source=sheet, sourceRows=rows, xcols=keyCols, ycols=numericCols([cursorCol])))', 'plot current numeric column vs key columns; numeric key column is used for x-axis, while categorical key columns determine color')
|
218
366
|
Sheet.addCommand('g.', 'plot-numerics', 'vd.push(GraphSheet(sheet.name, "graph", source=sheet, sourceRows=rows, xcols=keyCols, ycols=numericCols(nonKeyVisibleCols)))', 'plot a graph of all visible numeric columns vs key columns')
|
367
|
+
ColumnsSheet.addCommand('g.', 'plot-source-selected', 'vd.push(GraphSheet(sheet.source[0].name, "graph", source=source[0], sourceRows=source[0].rows, xcols=source[0].keyCols, ycols=numericCols(selectedRows)))', 'plot a graph of all selected columns vs key columns on source sheet')
|
219
368
|
|
220
369
|
# swap directions of up/down
|
221
370
|
InvertedCanvas.addCommand(None, 'go-up', 'if cursorBox: sheet.cursorBox.ymin += cursorBox.h', 'move cursor up by its height')
|
@@ -247,6 +396,12 @@ def set_x(sheet, s):
|
|
247
396
|
Canvas.addCommand('y', 'resize-y-input', 'sheet.set_y(input("set ymin ymax="))', 'set ymin/ymax on graph axes')
|
248
397
|
Canvas.addCommand('x', 'resize-x-input', 'sheet.set_x(input("set xmin xmax="))', 'set xmin/xmax on graph axes')
|
249
398
|
|
399
|
+
GraphSheet.addCommand('gx', 'draw-refline-x', 'sheet.draw_refline_x()', 'draw a vertical line at x-values (space-separated)')
|
400
|
+
GraphSheet.addCommand('gy', 'draw-refline-y', 'sheet.draw_refline_y()', 'draw a horizontal line at y-values (space-separated)')
|
401
|
+
GraphSheet.addCommand('zx', 'erase-refline-x', 'sheet.erase_refline_x()', 'remove a horizontal line at x-values (space-separated)')
|
402
|
+
GraphSheet.addCommand('zy', 'erase-refline-y', 'sheet.erase_refline_y()', 'remove a vertical line at y-values (space-separated)')
|
403
|
+
GraphSheet.addCommand('gzx', 'erase-reflines-x', 'sheet.reflines_x = []; sheet.refresh()', 'erase all vertical x-value lines')
|
404
|
+
GraphSheet.addCommand('gzy', 'erase-reflines-y', 'sheet.reflines_y = []; sheet.refresh()', 'erase any horizontal y-value lines')
|
250
405
|
|
251
406
|
vd.addGlobals({
|
252
407
|
'GraphSheet': GraphSheet,
|
@@ -256,4 +411,10 @@ vd.addGlobals({
|
|
256
411
|
vd.addMenuItems('''
|
257
412
|
Plot > Graph > current column > plot-column
|
258
413
|
Plot > Graph > all numeric columns > plot-numerics
|
414
|
+
Plot > Refline > draw at x coord (vert) > draw-refline-x
|
415
|
+
Plot > Refline > draw at y coord (horiz) > draw-refline-y
|
416
|
+
Plot > Refline > erase at x coord (vert) > erase-refline-x
|
417
|
+
Plot > Refline > erase at y coord (horiz) > erase-refline-y
|
418
|
+
Plot > Refline > erase all x (vert) > erase-reflines-x
|
419
|
+
Plot > Refline > erase all y (horiz) > erase-reflines-y
|
259
420
|
''')
|
visidata/guide.py
CHANGED
@@ -8,7 +8,8 @@ Each guide shows you how to use a particular feature in VisiData. Gray guides ha
|
|
8
8
|
import re
|
9
9
|
|
10
10
|
from visidata import vd, BaseSheet, Sheet, ItemColumn, Column, VisiData, ENTER, RowColorizer, AttrDict, MissingAttrFormatter
|
11
|
-
from visidata import wraptext
|
11
|
+
from visidata import wraptext, Path, CellColorizer
|
12
|
+
import visidata
|
12
13
|
|
13
14
|
guides_list = '''
|
14
15
|
GuideIndex ("A Guide to VisiData Guides (you are here)")
|
@@ -17,10 +18,11 @@ MenuGuide ("The VisiData Menu System")
|
|
17
18
|
CommandsSheet ("How to find the command you want run")
|
18
19
|
|
19
20
|
# real barebones basics
|
20
|
-
MovementGuide ("Movement and
|
21
|
+
MovementGuide ("Movement and search")
|
21
22
|
InputGuide ("Input keystrokes")
|
22
23
|
SortGuide ("Sorting")
|
23
|
-
|
24
|
+
ColumnsGuide ("Resize, hide, and rename columns")
|
25
|
+
TypesSheet ("Column types")
|
24
26
|
CommandLog ("Undo and Replay")
|
25
27
|
|
26
28
|
# rev this thing up
|
@@ -45,7 +47,7 @@ AggregatorsSheet ("Aggregations like sum, mean, and distinct")
|
|
45
47
|
FrequencyTable ("Frequency Tables are how you GROUP BY")
|
46
48
|
PivotGuide ("Pivot Tables are just Frequency Tables with more columns")
|
47
49
|
MeltGuide ("Melt is just Unpivot")
|
48
|
-
|
50
|
+
JsonSheet ("Some special features for JSON") # with expand/contract, unfurl
|
49
51
|
RegexGuide ("Matching and Transforming Strings with Regex")
|
50
52
|
GraphSheet ("Basic scatterplots and other graphs")
|
51
53
|
WindowFunctionGuide ("Perform operations on groups of rows")
|
@@ -61,10 +63,15 @@ ColorSheet ("See available colors")
|
|
61
63
|
MacrosSheet ("Recording macros")
|
62
64
|
MemorySheet ("Making note of certain values")
|
63
65
|
|
66
|
+
# Specific use cases
|
67
|
+
|
68
|
+
XsvGuide ("CSV/TSV and other text-delimited formats")
|
69
|
+
GrepSheet ("Load output of grep-like tools")
|
70
|
+
|
64
71
|
# advanced usage and developers
|
65
72
|
|
66
73
|
ThreadsSheet ("Threads past and present")
|
67
|
-
|
74
|
+
DeveloperGuide ("Inspecting internal Python objects")
|
68
75
|
|
69
76
|
# appendices
|
70
77
|
|
@@ -74,8 +81,10 @@ InputEditorGuide ("Using the builtin line editor")
|
|
74
81
|
vd.guides = {} # name -> guidecls
|
75
82
|
|
76
83
|
@VisiData.api
|
77
|
-
def addGuide(vd, name
|
78
|
-
vd.guides
|
84
|
+
def addGuide(vd, name):
|
85
|
+
guideSource = Path(vd.pkg_resources_files(visidata)/f'guides/{name}.md')
|
86
|
+
if guideSource.exists():
|
87
|
+
vd.guides[name] = GuideSheet(name, source=guideSource)
|
79
88
|
|
80
89
|
@VisiData.api
|
81
90
|
class GuideIndex(Sheet):
|
@@ -83,7 +92,7 @@ class GuideIndex(Sheet):
|
|
83
92
|
|
84
93
|
rowtype = 'guides' # rowdef: list(guide number, guide name, topic description, points, max_points)
|
85
94
|
columns = [
|
86
|
-
ItemColumn('n', 0, type=int),
|
95
|
+
ItemColumn('n', 0, width=0, type=int),
|
87
96
|
ItemColumn('name', 1, width=0),
|
88
97
|
ItemColumn('topic', 2, width=60),
|
89
98
|
]
|
@@ -95,7 +104,9 @@ class GuideIndex(Sheet):
|
|
95
104
|
for line in guides_list.splitlines():
|
96
105
|
m = re.search(r'(\w+?) \("(.*)"\)', line)
|
97
106
|
if m:
|
98
|
-
|
107
|
+
guidename, description = list(m.groups())
|
108
|
+
vd.addGuide(guidename)
|
109
|
+
yield [i, guidename, description]
|
99
110
|
i += 1
|
100
111
|
|
101
112
|
def openRow(self, row):
|
@@ -103,25 +114,25 @@ class GuideIndex(Sheet):
|
|
103
114
|
return vd.getGuide(name)
|
104
115
|
|
105
116
|
class OptionHelpGetter:
|
106
|
-
'For easy and consistent formatting in sidebars and helpstrings, use {
|
117
|
+
'For easy and consistent formatting in sidebars and helpstrings, use {help.options.opt_name}.'
|
107
118
|
def __getattr__(self, optname):
|
108
119
|
opt = vd.options._get(optname, 'default')
|
109
|
-
return f'[:onclick options-sheet {optname}]
|
120
|
+
return f'[:onclick options-sheet {optname}][:longname_guide]{optname}[/][/]: {opt.helpstr} (default: {opt.value})'
|
110
121
|
|
111
122
|
|
112
123
|
class CommandHelpGetter:
|
113
|
-
'For easy and consistent formatting in sidebars and helpstrings, use {
|
124
|
+
'For easy and consistent formatting in sidebars and helpstrings, use {help.commands.long_name}.'
|
114
125
|
def __init__(self, cls):
|
115
126
|
self.cls = cls
|
116
127
|
self.helpsheet = vd.HelpSheet()
|
117
|
-
self.helpsheet.ensureLoaded()
|
128
|
+
vd.sync(self.helpsheet.ensureLoaded())
|
118
129
|
|
119
130
|
def __getattr__(self, k):
|
120
131
|
return self.__getitem__(k)
|
121
132
|
|
122
133
|
def __getitem__(self, k):
|
123
134
|
longname = k.replace('_', '-')
|
124
|
-
binding = self.helpsheet.revbinds.get(longname, [None])[0]
|
135
|
+
binding = self.helpsheet.revbinds.get(longname, [None])[0] or '<unbound>'
|
125
136
|
# cmddict has a SheetClass associated with each command
|
126
137
|
# go through all the parents of the Sheet type, to look for the command
|
127
138
|
for cls in self.cls.superclasses():
|
@@ -136,11 +147,11 @@ class CommandHelpGetter:
|
|
136
147
|
if not m:
|
137
148
|
m = re.search(r'input(\w*)\("', cmd.execstr, re.IGNORECASE)
|
138
149
|
if m:
|
139
|
-
inputtype = m.groups()[0].lower()
|
140
|
-
binding += f'
|
150
|
+
inputtype = m.groups()[0].lower() or 'input'
|
151
|
+
binding += f' <{inputtype}>'
|
141
152
|
|
142
153
|
helpstr = cmd.helpstr
|
143
|
-
return f'
|
154
|
+
return f'[:code]{binding}[/] ([:longname_guide]{longname}[/]) to {helpstr}'
|
144
155
|
|
145
156
|
|
146
157
|
class GuideSheet(Sheet):
|
@@ -151,14 +162,31 @@ class GuideSheet(Sheet):
|
|
151
162
|
ItemColumn('guide', 1, width=80, displayer='full'),
|
152
163
|
]
|
153
164
|
precious = False
|
154
|
-
|
155
|
-
sheettype = Sheet
|
165
|
+
colorizers = [CellColorizer(4, 'color_sidebar', lambda s,c,r,v: True)]
|
156
166
|
|
157
167
|
def iterload(self):
|
168
|
+
self.metadata = AttrDict(sheettype='Sheet')
|
169
|
+
text = self.source.open(mode='r').read()
|
158
170
|
winWidth = 78
|
159
|
-
|
171
|
+
|
172
|
+
# parsing front matter
|
173
|
+
sections = text.split('---\n', maxsplit=2)
|
174
|
+
for section in sections[:-1]:
|
175
|
+
for config in section.splitlines():
|
176
|
+
config = config.strip()
|
177
|
+
if config:
|
178
|
+
try:
|
179
|
+
key, val = config.split(': ', maxsplit=1)
|
180
|
+
except ValueError:
|
181
|
+
vd.fail('incorrect front matter syntax')
|
182
|
+
self.metadata[key] = val
|
183
|
+
|
184
|
+
# formatting text
|
185
|
+
helper = AttrDict(commands=CommandHelpGetter(vd.getGlobals()[self.metadata.sheettype]),
|
160
186
|
options=OptionHelpGetter())
|
161
|
-
guidetext = MissingAttrFormatter().format(
|
187
|
+
guidetext = MissingAttrFormatter().format(sections[-1], help=helper, vd=vd)
|
188
|
+
|
189
|
+
# parsing guide
|
162
190
|
for startingLine, text in enumerate(guidetext.splitlines()):
|
163
191
|
text = text.strip()
|
164
192
|
if text:
|
@@ -168,11 +196,10 @@ class GuideSheet(Sheet):
|
|
168
196
|
yield [startingLine+1, text]
|
169
197
|
|
170
198
|
|
171
|
-
|
172
199
|
@VisiData.api
|
173
200
|
def getGuide(vd, name): # -> GuideSheet()
|
174
201
|
if name in vd.guides:
|
175
|
-
return vd.guides[name]
|
202
|
+
return vd.guides[name]
|
176
203
|
vd.warning(f'no guide named {name}')
|
177
204
|
|
178
205
|
BaseSheet.addCommand('', 'open-guide-index', 'vd.push(GuideIndex("VisiData_Guide"))', 'open VisiData guides table of contents')
|
@@ -188,8 +215,15 @@ def inputKeys(vd, prompt):
|
|
188
215
|
|
189
216
|
@BaseSheet.api
|
190
217
|
def getCommandInfo(sheet, keys):
|
218
|
+
if not keys:
|
219
|
+
return ''
|
191
220
|
cmd = sheet.getCommand(keys)
|
192
|
-
|
221
|
+
if cmd:
|
222
|
+
return CommandHelpGetter(type(sheet))[cmd.longname]
|
223
|
+
else:
|
224
|
+
vd.warning(f'no command bound to {keys} on {sheet}')
|
225
|
+
|
226
|
+
GuideSheet.options.color_current_row = "underline"
|
193
227
|
|
194
228
|
vd.addCommand('', 'show-command-info', 'status(getCommandInfo(inputKeys("get command for keystrokes: ")))', 'show longname and helpstring for keybinding')
|
195
229
|
|
@@ -197,4 +231,6 @@ vd.addMenuItems('''
|
|
197
231
|
Help > VisiData Feature Guides > open-guide-index
|
198
232
|
''')
|
199
233
|
|
200
|
-
vd.
|
234
|
+
vd.optalias('guides', 'preplay', 'open-guide-index')
|
235
|
+
|
236
|
+
vd.addGlobals({"CommandHelpGetter": CommandHelpGetter, "OptionHelpGetter": OptionHelpGetter})
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# Copy, Cut and Paste...
|
2
|
+
|
3
|
+
## ...rows to/from the internal clipboard
|
4
|
+
|
5
|
+
- {help.commands.copy_row}
|
6
|
+
- {help.commands.copy_selected}
|
7
|
+
|
8
|
+
- {help.commands.cut_row}
|
9
|
+
- {help.commands.cut_selected}
|
10
|
+
|
11
|
+
- {help.commands.paste_after}
|
12
|
+
- {help.commands.paste_before}
|
13
|
+
|
14
|
+
## ...cells to/from the internal clipboard
|
15
|
+
|
16
|
+
- {help.commands.copy_cell}
|
17
|
+
- {help.commands.cut_cell}
|
18
|
+
- {help.commands.paste_cell}
|
19
|
+
|
20
|
+
## ...columns from the intenral clipboard
|
21
|
+
|
22
|
+
- {help.commands.copy_cells}
|
23
|
+
- {help.commands.cut_cells}
|
24
|
+
- {help.commands.setcol_clipboard}
|
25
|
+
|
26
|
+
If there are fewer values in the clipboard than in the selected target column, the clipboard values will be repeated in sequence.
|
27
|
+
|
28
|
+
## ....to/from the system clipboard
|
29
|
+
|
30
|
+
- {help.commands.syscopy_cell}
|
31
|
+
- {help.commands.syscopy_cells}
|
32
|
+
|
33
|
+
- {help.commands.syscopy_row}
|
34
|
+
- {help.commands.syscopy_selected}
|
35
|
+
|
36
|
+
- {help.commands.syspaste_cells}
|
37
|
+
- {help.commands.syspaste_cells_selected}
|
38
|
+
|
39
|
+
## Paste a new sheet from the system clipboard
|
40
|
+
|
41
|
+
- {help.commands.open_syspaste}
|
42
|
+
|
43
|
+
The paste command will prompt for the filetype, so the input data (from the clipboard) can be in any supported format.
|
44
|
+
|
45
|
+
## Relevant options
|
46
|
+
|
47
|
+
- {help.options.clipboard_copy_cmd}
|
48
|
+
- {help.options.clipboard_paste_cmd}
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# Resize, hide, and rename columns
|
2
|
+
|
3
|
+
## Resize the current column
|
4
|
+
|
5
|
+
- {help.commands.resize_col_max}
|
6
|
+
- {help.commands.resize_col_input}
|
7
|
+
- {help.commands.resize_col_half}
|
8
|
+
|
9
|
+
## Resize multiple columns
|
10
|
+
|
11
|
+
- {help.commands.resize_cols_input}
|
12
|
+
|
13
|
+
## Hide and Unhide columns
|
14
|
+
|
15
|
+
Clean up the sheet by hiding unwanted columns. Hidden columns are still present, but not visible in the main set of columns.
|
16
|
+
(They are minimized on the right side and can be unhidden with {help.commands.resize_col_max}.
|
17
|
+
|
18
|
+
- {help.commands.hide_col}
|
19
|
+
- {help.commands.unhide_cols}
|
20
|
+
|
21
|
+
Use {help.commands.columns_sheet} to control visibility as well.
|
22
|
+
|
23
|
+
## Rename columns
|
24
|
+
|
25
|
+
- {help.commands.rename_col}
|
26
|
+
|
27
|
+
## Rename one or more columns using data
|
28
|
+
|
29
|
+
Rename one or more columns using data from the sheet:
|
30
|
+
|
31
|
+
- {help.commands.rename_col_selected}
|
32
|
+
- {help.commands.rename_cols_row}
|
33
|
+
- {help.commands.rename_cols_selected}
|
34
|
+
|
35
|
+
## Clean column names
|
36
|
+
|
37
|
+
Rename columns to be valid Python identifiers that can be used in column
|
38
|
+
expressions.
|
39
|
+
|
40
|
+
- {help.commands.normalize_col_names}
|
41
|
+
- {help.commands.clean_names}
|
42
|
+
|
43
|
+
## Change row height
|
44
|
+
|
45
|
+
Sometimes the data for a cell will not fit within the current width. If the
|
46
|
+
data is large, or spans multiple lines, change the row height to show more for
|
47
|
+
the current column width. Row height adjustments can also be combined with
|
48
|
+
column width changes.
|
49
|
+
|
50
|
+
- {help.commands.toggle_multiline}
|
51
|
+
- {help.commands.resize_height_input}
|
52
|
+
- {help.commands.resize_height_max}
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# How to find commands
|
2
|
+
|
3
|
+
## Command Palette
|
4
|
+
|
5
|
+
- {help.commands.exec_longname}
|
6
|
+
|
7
|
+
Start typing a command longname or keyword in its helpstring.
|
8
|
+
|
9
|
+
- [:code]Enter[/] to execute top command.
|
10
|
+
- [:code]Tab[/] to highlight top command and provide a numeric jumplist.
|
11
|
+
|
12
|
+
When a command is highlighted:
|
13
|
+
|
14
|
+
- [:code]Tab[/]/[:code]Shift+Tab[/] to cycle highlighted command.
|
15
|
+
- [:code]Enter[/] to execute highlighted command.
|
16
|
+
- [:code]0-9[/] to execute numbered command.
|
17
|
+
|
18
|
+
## Command Sheet
|
19
|
+
|
20
|
+
The Command Sheet lists all of the available commands, their keyboard bindings, function names, and other information.
|
21
|
+
|
22
|
+
- {help.commands.help_commands_all}
|
23
|
+
- {help.commands.help_commands}
|
24
|
+
|
25
|
+
'description' and 'longname' column: useful to find rows that share a longname suffix or prefix. E.g, `save` or `selected`.
|
26
|
+
|
27
|
+
'keystrokes' and 'all_bindings' column: useful to search for keybindings. E.g, `Ctrl` or `F`.
|
28
|
+
|
@@ -0,0 +1,34 @@
|
|
1
|
+
---
|
2
|
+
sheettype: DirSheet
|
3
|
+
---
|
4
|
+
# Directory Sheet
|
5
|
+
|
6
|
+
The **DirSheet** is a display of files and folders in the *source* directory.
|
7
|
+
|
8
|
+
To load a **DirSheet**:
|
9
|
+
|
10
|
+
- provide a directory as a source on the CLI (e.g. [:code]vd sample_data/[/])
|
11
|
+
- {help.commands.open_dir_current} (equivalent to [:code]vd .[/])
|
12
|
+
|
13
|
+
Use the **DirSheet** to find and open files:
|
14
|
+
|
15
|
+
- {help.commands.open_row_file}
|
16
|
+
- {help.commands.open_rows}
|
17
|
+
- {help.commands.open_dir_parent}
|
18
|
+
- {help.commands.sysopen_row}
|
19
|
+
|
20
|
+
Use the **DirSheet** as a file manager:
|
21
|
+
|
22
|
+
- {help.commands.copy_row}
|
23
|
+
- {help.commands.copy_selected}
|
24
|
+
- [:keys]d[/] ([:longname]delete-row[/]) to delete file from filesystem
|
25
|
+
- [:keys]e[/] ([:longname]edit-cell[/]) to change file metadata (all of the file metadata, except for *filetype*, is modifiable)
|
26
|
+
|
27
|
+
Modifications on the **DirSheet** are deferred - they do not take effect on the filesystem itself until they are confirmed.
|
28
|
+
- {help.commands.commit_sheet}
|
29
|
+
|
30
|
+
## Options of Interest (must reload to take effect)
|
31
|
+
|
32
|
+
- pass the flag [:code]-r[/] ([:code]--recursive[/]) on the CLI to display all of the files in all subfolders
|
33
|
+
- {help.options.dir_depth}
|
34
|
+
- {help.options.dir_hidden}
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# What was that error?
|
2
|
+
|
3
|
+
Status messages include [:warning]warnings[/] and [:error]errors[/].
|
4
|
+
|
5
|
+
A command may issue a [:warning]warning[/] status and continue running.
|
6
|
+
A command that [:warning]fails[/] or [:error]errors[/] is aborted.
|
7
|
+
|
8
|
+
## Investigating errors further
|
9
|
+
|
10
|
+
If a Python Exception like [:error]RuntimeError[/] appears in the sidebar:
|
11
|
+
|
12
|
+
- {help.commands.error_recent}
|
13
|
+
- {help.commands.errors_all}
|
14
|
+
|
15
|
+
If [:note_type]{vd.options.disp_note_fmtexc}[/] or [:error]{vd.options.disp_note_getexc}[/] appear inside a cell, it indicates an error happened during calculation, type-conversion, or formatting. When the cursor is on an error cell:
|
16
|
+
|
17
|
+
- {help.commands.error_cell}
|