visidata 3.0.2__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 -202
- visidata/_open.py +4 -1
- visidata/_types.py +4 -3
- visidata/aggregators.py +88 -39
- visidata/apps/vdsql/_ibis.py +7 -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 +54 -20
- 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 +17 -2
- 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 +16 -3
- visidata/features/ping.py +16 -12
- visidata/features/regex.py +5 -5
- 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 +163 -12
- visidata/guide.py +57 -24
- 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 +14 -13
- visidata/mainloop.py +14 -6
- visidata/man/vd.1 +72 -39
- visidata/man/vd.txt +72 -41
- visidata/memory.py +15 -4
- 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/save.py +16 -9
- visidata/search.py +4 -4
- visidata/selection.py +10 -56
- visidata/settings.py +37 -35
- visidata/sheets.py +186 -108
- visidata/shell.py +22 -12
- 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 -5
- visidata/utils.py +9 -3
- visidata/vdobj.py +21 -1
- visidata/wrappers.py +9 -1
- {visidata-3.0.2.data → visidata-3.1.data}/data/share/applications/visidata.desktop +2 -2
- {visidata-3.0.2.data → visidata-3.1.data}/data/share/man/man1/vd.1 +72 -39
- {visidata-3.0.2.data → visidata-3.1.data}/data/share/man/man1/visidata.1 +72 -39
- {visidata-3.0.2.dist-info → visidata-3.1.dist-info}/METADATA +24 -6
- visidata-3.1.dist-info/RECORD +284 -0
- visidata-3.0.2.dist-info/RECORD +0 -258
- {visidata-3.0.2.data → visidata-3.1.data}/scripts/vd +0 -0
- {visidata-3.0.2.data → visidata-3.1.data}/scripts/vd2to3.vdx +0 -0
- {visidata-3.0.2.dist-info → visidata-3.1.dist-info}/LICENSE.gpl3 +0 -0
- {visidata-3.0.2.dist-info → visidata-3.1.dist-info}/WHEEL +0 -0
- {visidata-3.0.2.dist-info → visidata-3.1.dist-info}/entry_points.txt +0 -0
- {visidata-3.0.2.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,7 +27,7 @@ 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()
|
25
31
|
|
26
32
|
def rowsWithin(self, plotter_bbox):
|
27
33
|
return super().rowsWithin(plotter_bbox, invert_y=True)
|
@@ -29,9 +35,10 @@ class InvertedCanvas(Canvas):
|
|
29
35
|
def zoomTo(self, bbox):
|
30
36
|
super().zoomTo(bbox)
|
31
37
|
self.fixPoint(Point(self.plotviewBox.xmin, self.plotviewBox.ymin),
|
32
|
-
Point(bbox.xmin, bbox.ymax
|
38
|
+
Point(bbox.xmin, bbox.ymax))
|
39
|
+
self.resetBounds()
|
33
40
|
|
34
|
-
def scaleY(self, canvasY):
|
41
|
+
def scaleY(self, canvasY) -> int:
|
35
42
|
'returns a plotter y coordinate for a canvas y coordinate, with the y direction inverted'
|
36
43
|
return self.plotviewBox.ymax-round((canvasY-self.visibleBox.ymin)*self.yScaler)
|
37
44
|
|
@@ -69,6 +76,11 @@ class GraphSheet(InvertedCanvas):
|
|
69
76
|
self.ylabel_maxw = 0
|
70
77
|
super().__init__(*names, **kwargs)
|
71
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
|
+
|
72
84
|
vd.numericCols(self.xcols) or vd.fail('at least one numeric key col necessary for x-axis')
|
73
85
|
self.ycols or vd.fail('%s is non-numeric' % '/'.join(yc.name for yc in kwargs.get('ycols')))
|
74
86
|
|
@@ -111,11 +123,45 @@ class GraphSheet(InvertedCanvas):
|
|
111
123
|
|
112
124
|
self.xzoomlevel=self.yzoomlevel=1.0
|
113
125
|
self.resetBounds()
|
114
|
-
self.refresh()
|
115
126
|
|
116
|
-
def
|
117
|
-
|
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)
|
118
162
|
self.createLabels()
|
163
|
+
if refresh:
|
164
|
+
self.refresh()
|
119
165
|
|
120
166
|
def moveToRow(self, rowstr):
|
121
167
|
ymin, ymax = map(float, map(self.parseY, rowstr.split()))
|
@@ -123,6 +169,43 @@ class GraphSheet(InvertedCanvas):
|
|
123
169
|
self.cursorBox.h = ymax-ymin
|
124
170
|
return True
|
125
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
|
+
|
126
209
|
def moveToCol(self, colstr):
|
127
210
|
xmin, xmax = map(float, map(self.parseX, colstr.split()))
|
128
211
|
self.cursorBox.xmin = xmin
|
@@ -163,20 +246,23 @@ class GraphSheet(InvertedCanvas):
|
|
163
246
|
return self.ycols[0].type(txt)
|
164
247
|
|
165
248
|
def add_y_axis_label(self, frac):
|
166
|
-
|
249
|
+
label_data_y = self.visibleBox.ymin + frac*self.visibleBox.h
|
250
|
+
txt = self.formatYLabel(label_data_y)
|
167
251
|
w = (dispwidth(txt)+1)*2
|
168
252
|
if self.ylabel_maxw < w:
|
169
253
|
self.ylabel_maxw = w
|
254
|
+
y = self.scaleY(label_data_y)
|
170
255
|
|
171
256
|
# plot y-axis labels on the far left of the canvas, but within the plotview height-wise
|
172
|
-
self.plotlabel(0,
|
257
|
+
self.plotlabel(0, y, txt, 'graph_axis')
|
173
258
|
|
174
259
|
def add_x_axis_label(self, frac):
|
175
|
-
|
260
|
+
label_data_x = self.visibleBox.xmin + frac*self.visibleBox.w
|
261
|
+
txt = self.formatXLabel(label_data_x)
|
176
262
|
tick = vd.options.disp_graph_tick_x or ''
|
177
263
|
|
178
264
|
# plot x-axis labels below the plotviewBox.ymax, but within the plotview width-wise
|
179
|
-
x = self.
|
265
|
+
x = self.scaleX(label_data_x)
|
180
266
|
|
181
267
|
if frac < 1.0:
|
182
268
|
txt = tick + txt
|
@@ -223,9 +309,62 @@ class GraphSheet(InvertedCanvas):
|
|
223
309
|
rows = super().rowsWithin(plotter_bbox)
|
224
310
|
return sorted(rows, key=lambda r: self.row_order[self.source.rowid(r)])
|
225
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
|
+
|
226
364
|
|
227
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')
|
228
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')
|
229
368
|
|
230
369
|
# swap directions of up/down
|
231
370
|
InvertedCanvas.addCommand(None, 'go-up', 'if cursorBox: sheet.cursorBox.ymin += cursorBox.h', 'move cursor up by its height')
|
@@ -257,6 +396,12 @@ def set_x(sheet, s):
|
|
257
396
|
Canvas.addCommand('y', 'resize-y-input', 'sheet.set_y(input("set ymin ymax="))', 'set ymin/ymax on graph axes')
|
258
397
|
Canvas.addCommand('x', 'resize-x-input', 'sheet.set_x(input("set xmin xmax="))', 'set xmin/xmax on graph axes')
|
259
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')
|
260
405
|
|
261
406
|
vd.addGlobals({
|
262
407
|
'GraphSheet': GraphSheet,
|
@@ -266,4 +411,10 @@ vd.addGlobals({
|
|
266
411
|
vd.addMenuItems('''
|
267
412
|
Plot > Graph > current column > plot-column
|
268
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
|
269
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)")
|
@@ -20,7 +21,7 @@ CommandsSheet ("How to find the command you want run")
|
|
20
21
|
MovementGuide ("Movement and search")
|
21
22
|
InputGuide ("Input keystrokes")
|
22
23
|
SortGuide ("Sorting")
|
23
|
-
ColumnsGuide ("
|
24
|
+
ColumnsGuide ("Resize, hide, and rename columns")
|
24
25
|
TypesSheet ("Column types")
|
25
26
|
CommandLog ("Undo and Replay")
|
26
27
|
|
@@ -46,7 +47,7 @@ AggregatorsSheet ("Aggregations like sum, mean, and distinct")
|
|
46
47
|
FrequencyTable ("Frequency Tables are how you GROUP BY")
|
47
48
|
PivotGuide ("Pivot Tables are just Frequency Tables with more columns")
|
48
49
|
MeltGuide ("Melt is just Unpivot")
|
49
|
-
|
50
|
+
JsonSheet ("Some special features for JSON") # with expand/contract, unfurl
|
50
51
|
RegexGuide ("Matching and Transforming Strings with Regex")
|
51
52
|
GraphSheet ("Basic scatterplots and other graphs")
|
52
53
|
WindowFunctionGuide ("Perform operations on groups of rows")
|
@@ -62,10 +63,15 @@ ColorSheet ("See available colors")
|
|
62
63
|
MacrosSheet ("Recording macros")
|
63
64
|
MemorySheet ("Making note of certain values")
|
64
65
|
|
66
|
+
# Specific use cases
|
67
|
+
|
68
|
+
XsvGuide ("CSV/TSV and other text-delimited formats")
|
69
|
+
GrepSheet ("Load output of grep-like tools")
|
70
|
+
|
65
71
|
# advanced usage and developers
|
66
72
|
|
67
73
|
ThreadsSheet ("Threads past and present")
|
68
|
-
|
74
|
+
DeveloperGuide ("Inspecting internal Python objects")
|
69
75
|
|
70
76
|
# appendices
|
71
77
|
|
@@ -75,8 +81,10 @@ InputEditorGuide ("Using the builtin line editor")
|
|
75
81
|
vd.guides = {} # name -> guidecls
|
76
82
|
|
77
83
|
@VisiData.api
|
78
|
-
def addGuide(vd, name
|
79
|
-
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)
|
80
88
|
|
81
89
|
@VisiData.api
|
82
90
|
class GuideIndex(Sheet):
|
@@ -84,7 +92,7 @@ class GuideIndex(Sheet):
|
|
84
92
|
|
85
93
|
rowtype = 'guides' # rowdef: list(guide number, guide name, topic description, points, max_points)
|
86
94
|
columns = [
|
87
|
-
ItemColumn('n', 0, type=int),
|
95
|
+
ItemColumn('n', 0, width=0, type=int),
|
88
96
|
ItemColumn('name', 1, width=0),
|
89
97
|
ItemColumn('topic', 2, width=60),
|
90
98
|
]
|
@@ -96,7 +104,9 @@ class GuideIndex(Sheet):
|
|
96
104
|
for line in guides_list.splitlines():
|
97
105
|
m = re.search(r'(\w+?) \("(.*)"\)', line)
|
98
106
|
if m:
|
99
|
-
|
107
|
+
guidename, description = list(m.groups())
|
108
|
+
vd.addGuide(guidename)
|
109
|
+
yield [i, guidename, description]
|
100
110
|
i += 1
|
101
111
|
|
102
112
|
def openRow(self, row):
|
@@ -104,25 +114,25 @@ class GuideIndex(Sheet):
|
|
104
114
|
return vd.getGuide(name)
|
105
115
|
|
106
116
|
class OptionHelpGetter:
|
107
|
-
'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}.'
|
108
118
|
def __getattr__(self, optname):
|
109
119
|
opt = vd.options._get(optname, 'default')
|
110
|
-
return f'[:onclick options-sheet {optname}][:
|
120
|
+
return f'[:onclick options-sheet {optname}][:longname_guide]{optname}[/][/]: {opt.helpstr} (default: {opt.value})'
|
111
121
|
|
112
122
|
|
113
123
|
class CommandHelpGetter:
|
114
|
-
'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}.'
|
115
125
|
def __init__(self, cls):
|
116
126
|
self.cls = cls
|
117
127
|
self.helpsheet = vd.HelpSheet()
|
118
|
-
self.helpsheet.ensureLoaded()
|
128
|
+
vd.sync(self.helpsheet.ensureLoaded())
|
119
129
|
|
120
130
|
def __getattr__(self, k):
|
121
131
|
return self.__getitem__(k)
|
122
132
|
|
123
133
|
def __getitem__(self, k):
|
124
134
|
longname = k.replace('_', '-')
|
125
|
-
binding = self.helpsheet.revbinds.get(longname, [None])[0]
|
135
|
+
binding = self.helpsheet.revbinds.get(longname, [None])[0] or '<unbound>'
|
126
136
|
# cmddict has a SheetClass associated with each command
|
127
137
|
# go through all the parents of the Sheet type, to look for the command
|
128
138
|
for cls in self.cls.superclasses():
|
@@ -137,11 +147,11 @@ class CommandHelpGetter:
|
|
137
147
|
if not m:
|
138
148
|
m = re.search(r'input(\w*)\("', cmd.execstr, re.IGNORECASE)
|
139
149
|
if m:
|
140
|
-
inputtype = m.groups()[0].lower()
|
141
|
-
binding += f'
|
150
|
+
inputtype = m.groups()[0].lower() or 'input'
|
151
|
+
binding += f' <{inputtype}>'
|
142
152
|
|
143
153
|
helpstr = cmd.helpstr
|
144
|
-
return f'[:
|
154
|
+
return f'[:code]{binding}[/] ([:longname_guide]{longname}[/]) to {helpstr}'
|
145
155
|
|
146
156
|
|
147
157
|
class GuideSheet(Sheet):
|
@@ -152,14 +162,31 @@ class GuideSheet(Sheet):
|
|
152
162
|
ItemColumn('guide', 1, width=80, displayer='full'),
|
153
163
|
]
|
154
164
|
precious = False
|
155
|
-
|
156
|
-
sheettype = Sheet
|
165
|
+
colorizers = [CellColorizer(4, 'color_sidebar', lambda s,c,r,v: True)]
|
157
166
|
|
158
167
|
def iterload(self):
|
168
|
+
self.metadata = AttrDict(sheettype='Sheet')
|
169
|
+
text = self.source.open(mode='r').read()
|
159
170
|
winWidth = 78
|
160
|
-
|
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]),
|
161
186
|
options=OptionHelpGetter())
|
162
|
-
guidetext = MissingAttrFormatter().format(
|
187
|
+
guidetext = MissingAttrFormatter().format(sections[-1], help=helper, vd=vd)
|
188
|
+
|
189
|
+
# parsing guide
|
163
190
|
for startingLine, text in enumerate(guidetext.splitlines()):
|
164
191
|
text = text.strip()
|
165
192
|
if text:
|
@@ -169,11 +196,10 @@ class GuideSheet(Sheet):
|
|
169
196
|
yield [startingLine+1, text]
|
170
197
|
|
171
198
|
|
172
|
-
|
173
199
|
@VisiData.api
|
174
200
|
def getGuide(vd, name): # -> GuideSheet()
|
175
201
|
if name in vd.guides:
|
176
|
-
return vd.guides[name]
|
202
|
+
return vd.guides[name]
|
177
203
|
vd.warning(f'no guide named {name}')
|
178
204
|
|
179
205
|
BaseSheet.addCommand('', 'open-guide-index', 'vd.push(GuideIndex("VisiData_Guide"))', 'open VisiData guides table of contents')
|
@@ -189,8 +215,15 @@ def inputKeys(vd, prompt):
|
|
189
215
|
|
190
216
|
@BaseSheet.api
|
191
217
|
def getCommandInfo(sheet, keys):
|
218
|
+
if not keys:
|
219
|
+
return ''
|
192
220
|
cmd = sheet.getCommand(keys)
|
193
|
-
|
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"
|
194
227
|
|
195
228
|
vd.addCommand('', 'show-command-info', 'status(getCommandInfo(inputKeys("get command for keystrokes: ")))', 'show longname and helpstring for keybinding')
|
196
229
|
|
@@ -200,4 +233,4 @@ vd.addMenuItems('''
|
|
200
233
|
|
201
234
|
vd.optalias('guides', 'preplay', 'open-guide-index')
|
202
235
|
|
203
|
-
vd.addGlobals({
|
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}
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# Frequency Tables are how you GROUP BY
|
2
|
+
|
3
|
+
Frequency Tables group rows into bins by column value, and includes summary columns for source columns with aggregators.
|
4
|
+
|
5
|
+
Set `--numeric-binning` to bin numeric rows into ranges instead of discrete values.
|
6
|
+
|
7
|
+
- {help.commands.freq_col}
|
8
|
+
|
9
|
+
- {help.commands.freq_keys}
|
10
|
+
|
11
|
+
- {help.commands.freq_summary}
|
12
|
+
|
13
|
+
## Aggregators
|
14
|
+
|
15
|
+
A **Frequency Table** contains a summary columns for each aggregator added to a source column.
|
16
|
+
These aggregators need to be added before creating the Frequency Table.
|
17
|
+
Examples of aggregators include min, max, sum, distinct, count, and list.
|
18
|
+
|
19
|
+
- {help.commands.aggregate-col}
|
20
|
+
|
21
|
+
Note: set an appropriate type for the aggregator target column, for example {help.commands.type_float}.
|
22
|
+
|
23
|
+
## Explore the data
|
24
|
+
|
25
|
+
Dive into a group to see the underlying row(s) using the **Frequency Table**:
|
26
|
+
|
27
|
+
- {help.commands.open_row}
|
28
|
+
- {help.commands.dive_selected}
|
29
|
+
|
30
|
+
Select a group to select all of its underlying rows in the source sheet.
|
31
|
+
|
32
|
+
## Using Split Panes with Frequency Tables
|
33
|
+
|
34
|
+
Press `Shift+Z` to open a split pane, and then `Shift+F` to create a **Frequency Table**. The **Frequency Table** will automatically open in the other pane.
|
35
|
+
|
36
|
+
See the `SplitpanesGuide` for more.
|
37
|
+
|
38
|
+
#### Options
|
39
|
+
|
40
|
+
- {help.options.disp_histogram}
|
41
|
+
- {help.options.histogram_bins}
|
42
|
+
- {help.options.numeric_binning}
|