visidata 2.11.1__py3-none-any.whl → 3.0.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 +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 +78 -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 +63 -51
- 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 +6 -2
- 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 +22 -4
- 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 +197 -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} +77 -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 +200 -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 +20 -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 +54 -12
- 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 +302 -149
- visidata/man/vd.txt +291 -154
- 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 +55 -205
- 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 +239 -260
- visidata/shell.py +51 -21
- visidata/sidebar.py +162 -0
- visidata/sort.py +11 -4
- visidata/statusbar.py +114 -104
- visidata/stored_list.py +43 -0
- visidata/stored_prop.py +38 -0
- visidata/tests/benchmark.csv +52 -0
- visidata/tests/conftest.py +3 -3
- visidata/tests/test_cliptext.py +39 -0
- visidata/tests/test_commands.py +65 -7
- visidata/tests/test_edittext.py +2 -2
- visidata/tests/test_features.py +28 -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 +89 -40
- 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.1.data/data/share/applications/visidata.desktop +7 -0
- {visidata-2.11.1.data → visidata-3.0.1.data}/data/share/man/man1/vd.1 +302 -149
- {visidata-2.11.1.data → visidata-3.0.1.data}/data/share/man/man1/visidata.1 +302 -149
- visidata-3.0.1.data/scripts/vd2to3.vdx +9 -0
- {visidata-2.11.1.dist-info → visidata-3.0.1.dist-info}/METADATA +12 -8
- visidata-3.0.1.dist-info/RECORD +258 -0
- {visidata-2.11.1.dist-info → visidata-3.0.1.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.1.data}/scripts/vd +0 -0
- {visidata-2.11.1.dist-info → visidata-3.0.1.dist-info}/LICENSE.gpl3 +0 -0
- {visidata-2.11.1.dist-info → visidata-3.0.1.dist-info}/entry_points.txt +0 -0
- {visidata-2.11.1.dist-info → visidata-3.0.1.dist-info}/top_level.txt +0 -0
visidata/canvas.py
CHANGED
@@ -2,18 +2,19 @@ import math
|
|
2
2
|
import random
|
3
3
|
|
4
4
|
from collections import defaultdict, Counter, OrderedDict
|
5
|
-
from visidata import
|
5
|
+
from visidata import vd, asyncthread, ENTER, colors, update_attr, clipdraw, dispwidth
|
6
|
+
from visidata import BaseSheet, Column, Progress, ColorAttr
|
6
7
|
from visidata.bezier import bezier
|
7
8
|
|
8
9
|
# see www/design/graphics.md
|
9
10
|
|
10
|
-
vd.
|
11
|
-
vd.
|
12
|
-
vd.
|
13
|
-
vd.
|
14
|
-
vd.
|
15
|
-
vd.
|
16
|
-
vd.
|
11
|
+
vd.theme_option('disp_graph_labels', True, 'show axes and legend on graph')
|
12
|
+
vd.theme_option('plot_colors', 'green red yellow cyan magenta white 38 136 168', 'list of distinct colors to use for plotting distinct objects')
|
13
|
+
vd.theme_option('disp_canvas_charset', ''.join(chr(0x2800+i) for i in range(256)), 'charset to render 2x4 blocks on canvas')
|
14
|
+
vd.theme_option('disp_pixel_random', False, 'randomly choose attr from set of pixels instead of most common')
|
15
|
+
vd.theme_option('disp_zoom_incr', 2.0, 'amount to multiply current zoomlevel when zooming')
|
16
|
+
vd.theme_option('color_graph_hidden', '238 blue', 'color of legend for hidden attribute')
|
17
|
+
vd.theme_option('color_graph_selected', 'bold', 'color of selected graph points')
|
17
18
|
|
18
19
|
|
19
20
|
class Point:
|
@@ -158,18 +159,19 @@ class Plotter(BaseSheet):
|
|
158
159
|
# pixels[y][x] = { attr: list(rows), ... }
|
159
160
|
self.pixels = [[defaultdict(list) for x in range(self.plotwidth)] for y in range(self.plotheight)]
|
160
161
|
|
161
|
-
def plotpixel(self, x, y, attr=
|
162
|
+
def plotpixel(self, x, y, attr:"str|ColorAttr=''", row=None):
|
162
163
|
self.pixels[y][x][attr].append(row)
|
163
164
|
|
164
|
-
def plotline(self, x1, y1, x2, y2, attr=
|
165
|
+
def plotline(self, x1, y1, x2, y2, attr:"str|ColorAttr=''", row=None):
|
165
166
|
for x, y in iterline(x1, y1, x2, y2):
|
166
167
|
self.plotpixel(math.ceil(x), math.ceil(y), attr, row)
|
167
168
|
|
168
|
-
def plotlabel(self, x, y, text, attr=
|
169
|
+
def plotlabel(self, x, y, text, attr:"str|ColorAttr=''", row=None):
|
169
170
|
self.labels.append((x, y, text, attr, row))
|
170
171
|
|
171
|
-
def plotlegend(self, i, txt, attr=
|
172
|
-
|
172
|
+
def plotlegend(self, i, txt, attr:"str|ColorAttr=''", width=15):
|
173
|
+
# move it 1 character to the left b/c the rightmost column can't be drawn to
|
174
|
+
self.plotlabel(self.plotwidth-(width+1)*2, i*4, txt, attr)
|
173
175
|
|
174
176
|
@property
|
175
177
|
def plotterCursorBox(self):
|
@@ -183,14 +185,14 @@ class Plotter(BaseSheet):
|
|
183
185
|
def plotterFromTerminalCoord(self, x, y):
|
184
186
|
return x*2, y*4
|
185
187
|
|
186
|
-
def getPixelAttrRandom(self, x, y):
|
187
|
-
'weighted-random choice of
|
188
|
+
def getPixelAttrRandom(self, x, y) -> str:
|
189
|
+
'weighted-random choice of colornum at this pixel.'
|
188
190
|
c = list(attr for attr, rows in self.pixels[y][x].items()
|
189
191
|
for r in rows if attr and attr not in self.hiddenAttrs)
|
190
192
|
return random.choice(c) if c else 0
|
191
193
|
|
192
|
-
def getPixelAttrMost(self, x, y):
|
193
|
-
'most common
|
194
|
+
def getPixelAttrMost(self, x, y) -> str:
|
195
|
+
'most common colornum at this pixel.'
|
194
196
|
r = self.pixels[y][x]
|
195
197
|
if not r:
|
196
198
|
return 0
|
@@ -198,22 +200,24 @@ class Plotter(BaseSheet):
|
|
198
200
|
if not c:
|
199
201
|
return 0
|
200
202
|
_, attr, rows = max(c)
|
201
|
-
if isinstance(self.source, BaseSheet) and anySelected(self.source, rows):
|
202
|
-
attr = update_attr(ColorAttr(attr, 0, 8, attr), colors.color_graph_selected, 10).attr
|
203
203
|
return attr
|
204
204
|
|
205
|
-
def hideAttr(self, attr, hide=True):
|
205
|
+
def hideAttr(self, attr:str, hide=True):
|
206
206
|
if hide:
|
207
207
|
self.hiddenAttrs.add(attr)
|
208
208
|
else:
|
209
209
|
self.hiddenAttrs.remove(attr)
|
210
210
|
self.plotlegends()
|
211
211
|
|
212
|
-
def rowsWithin(self,
|
213
|
-
'return list of deduped rows within
|
212
|
+
def rowsWithin(self, plotter_bbox):
|
213
|
+
'return list of deduped rows within plotter_bbox'
|
214
214
|
ret = {}
|
215
|
-
|
216
|
-
|
215
|
+
x_start = max(0, plotter_bbox.xmin)
|
216
|
+
y_start = max(0, plotter_bbox.ymin)
|
217
|
+
y_end = min(len(self.pixels), plotter_bbox.ymax)
|
218
|
+
for y in range(y_start, y_end):
|
219
|
+
x_end = min(len(self.pixels[y]), plotter_bbox.xmax)
|
220
|
+
for x in range(x_start, x_end):
|
217
221
|
for attr, rows in self.pixels[y][x].items():
|
218
222
|
if attr not in self.hiddenAttrs:
|
219
223
|
for r in rows:
|
@@ -253,16 +257,17 @@ class Plotter(BaseSheet):
|
|
253
257
|
pow2 *= 2
|
254
258
|
|
255
259
|
if braille_num != 0:
|
256
|
-
|
260
|
+
color = Counter(c for c in block_attrs if c).most_common(1)[0][0]
|
261
|
+
cattr = colors.get_color(color)
|
257
262
|
else:
|
258
|
-
|
263
|
+
cattr = ColorAttr()
|
259
264
|
|
260
265
|
if cursorBBox.contains(char_x*2, char_y*4) or \
|
261
266
|
cursorBBox.contains(char_x*2+1, char_y*4+3):
|
262
|
-
|
267
|
+
cattr = update_attr(cattr, colors.color_current_row)
|
263
268
|
|
264
|
-
if attr:
|
265
|
-
scr.addstr(char_y, char_x, disp_canvas_charset[braille_num], attr)
|
269
|
+
if cattr.attr:
|
270
|
+
scr.addstr(char_y, char_x, disp_canvas_charset[braille_num], cattr.attr)
|
266
271
|
|
267
272
|
def _mark_overlap_text(labels, textobj):
|
268
273
|
def _overlaps(a, b):
|
@@ -283,7 +288,7 @@ class Plotter(BaseSheet):
|
|
283
288
|
o[1] = False
|
284
289
|
label_fldraw[1] = False
|
285
290
|
|
286
|
-
if self.options.
|
291
|
+
if self.options.disp_graph_labels:
|
287
292
|
labels_by_line = defaultdict(list) # y -> text labels
|
288
293
|
|
289
294
|
for pix_x, pix_y, txt, attr, row in self.labels:
|
@@ -300,7 +305,16 @@ class Plotter(BaseSheet):
|
|
300
305
|
for o, fldraw in line:
|
301
306
|
if fldraw:
|
302
307
|
char_x, char_y, txt, attr, row = o
|
303
|
-
|
308
|
+
cattr = colors.get_color(attr)
|
309
|
+
clipdraw(scr, char_y, char_x, txt, cattr, dispwidth(txt))
|
310
|
+
cursorBBox = self.plotterCursorBox
|
311
|
+
for c in txt:
|
312
|
+
w = dispwidth(c)
|
313
|
+
# check if the cursor contains the midpoint of the character box
|
314
|
+
if cursorBBox.contains(char_x*2+1, char_y*4+2):
|
315
|
+
char_attr = update_attr(cattr, colors.color_current_row)
|
316
|
+
clipdraw(scr, char_y, char_x, c, char_attr, w)
|
317
|
+
char_x += w
|
304
318
|
|
305
319
|
|
306
320
|
# - has a cursor, of arbitrary position and width/height (not restricted to current zoom)
|
@@ -309,10 +323,11 @@ class Canvas(Plotter):
|
|
309
323
|
rowtype = 'plots'
|
310
324
|
leftMarginPixels = 10*2
|
311
325
|
rightMarginPixels = 4*2
|
312
|
-
topMarginPixels = 0
|
326
|
+
topMarginPixels = 0*4
|
313
327
|
bottomMarginPixels = 1*4 # reserve bottom line for x axis
|
314
328
|
|
315
329
|
def __init__(self, *names, **kwargs):
|
330
|
+
self.left_margin = self.leftMarginPixels
|
316
331
|
super().__init__(*names, **kwargs)
|
317
332
|
|
318
333
|
self.canvasBox = None # bounding box of entire canvas, in canvas units
|
@@ -324,8 +339,8 @@ class Canvas(Plotter):
|
|
324
339
|
self.yzoomlevel = 1.0
|
325
340
|
self.needsRefresh = False
|
326
341
|
|
327
|
-
self.polylines = [] # list of ([(canvas_x, canvas_y), ...],
|
328
|
-
self.gridlabels = [] # list of (grid_x, grid_y, label,
|
342
|
+
self.polylines = [] # list of ([(canvas_x, canvas_y), ...], fgcolornum, row)
|
343
|
+
self.gridlabels = [] # list of (grid_x, grid_y, label, fgcolornum, row)
|
329
344
|
|
330
345
|
self.legends = OrderedDict() # txt: attr (visible legends only)
|
331
346
|
self.plotAttrs = {} # key: attr (all keys, for speed)
|
@@ -338,12 +353,13 @@ class Canvas(Plotter):
|
|
338
353
|
def reset(self):
|
339
354
|
'clear everything in preparation for a fresh reload()'
|
340
355
|
self.polylines.clear()
|
356
|
+
self.left_margin = self.leftMarginPixels
|
341
357
|
self.legends.clear()
|
342
358
|
self.legendwidth = 0
|
343
359
|
self.plotAttrs.clear()
|
344
|
-
self.unusedAttrs = list(
|
360
|
+
self.unusedAttrs = list(self.options.plot_colors.split())
|
345
361
|
|
346
|
-
def plotColor(self, k):
|
362
|
+
def plotColor(self, k) -> str:
|
347
363
|
attr = self.plotAttrs.get(k, None)
|
348
364
|
if attr is None:
|
349
365
|
if self.unusedAttrs:
|
@@ -354,16 +370,38 @@ class Canvas(Plotter):
|
|
354
370
|
del self.legends[lastlegend]
|
355
371
|
legend = '[other]'
|
356
372
|
|
357
|
-
self.legendwidth = max(self.legendwidth,
|
373
|
+
self.legendwidth = max(self.legendwidth, dispwidth(legend))
|
358
374
|
self.legends[legend] = attr
|
359
375
|
self.plotAttrs[k] = attr
|
360
|
-
self.plotlegends()
|
361
376
|
return attr
|
362
377
|
|
363
378
|
def resetCanvasDimensions(self, windowHeight, windowWidth):
|
379
|
+
old_plotsize = None
|
380
|
+
realign_cursor = False
|
381
|
+
if hasattr(self, 'plotwidth') and hasattr(self, 'plotheight'):
|
382
|
+
old_plotsize = [self.plotheight, self.plotwidth]
|
383
|
+
if hasattr(self, 'cursorBox') and self.cursorBox and self.visibleBox:
|
384
|
+
# if the cursor is at the origin, realign it with the origin after the resize
|
385
|
+
if self.cursorBox.xmin == self.visibleBox.xmin and self.cursorBox.ymin == self.calcBottomCursorY():
|
386
|
+
realign_cursor = True
|
364
387
|
super().resetCanvasDimensions(windowHeight, windowWidth)
|
365
|
-
|
366
|
-
|
388
|
+
if hasattr(self, 'legendwidth'):
|
389
|
+
# +4 = 1 empty space after the graph + 2 characters for the legend prefixes of "1:", "2:", etc +
|
390
|
+
# 1 character for the empty rightmost column
|
391
|
+
new_margin = max(self.rightMarginPixels, (self.legendwidth+4)*2)
|
392
|
+
pvbox_xmax = self.plotwidth-new_margin-1
|
393
|
+
# ensure the graph data takes up at least 3/4 of the width of the screen no matter how wide the legend gets
|
394
|
+
pvbox_xmax = max(pvbox_xmax, math.ceil(self.plotwidth * 3/4)//2*2 + 1)
|
395
|
+
else:
|
396
|
+
pvbox_xmax = self.plotwidth-self.rightMarginPixels-1
|
397
|
+
self.left_margin = min(self.left_margin, math.ceil(self.plotwidth * 1/3)//2*2)
|
398
|
+
self.plotviewBox = BoundingBox(self.left_margin, self.topMarginPixels,
|
399
|
+
pvbox_xmax, self.plotheight-self.bottomMarginPixels-1)
|
400
|
+
if [self.plotheight, self.plotwidth] != old_plotsize:
|
401
|
+
if hasattr(self, 'cursorBox') and self.cursorBox:
|
402
|
+
self.setCursorSizeInPlotterPixels(2, 4)
|
403
|
+
if realign_cursor:
|
404
|
+
self.cursorBox.ymin = self.calcBottomCursorY()
|
367
405
|
|
368
406
|
@property
|
369
407
|
def statusLine(self):
|
@@ -371,20 +409,29 @@ class Canvas(Plotter):
|
|
371
409
|
|
372
410
|
@property
|
373
411
|
def canvasMouse(self):
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
def canvasFromTerminalCoord(self, x, y):
|
380
|
-
return self.canvasFromPlotterCoord(*self.plotterFromTerminalCoord(x, y))
|
412
|
+
x = self.plotterMouse.x
|
413
|
+
y = self.plotterMouse.y
|
414
|
+
if not self.canvasBox: return None
|
415
|
+
p = Point(self.unscaleX(x), self.unscaleY(y))
|
416
|
+
return p
|
381
417
|
|
382
418
|
def setCursorSize(self, p):
|
383
419
|
'sets width based on diagonal corner p'
|
420
|
+
if not p: return
|
384
421
|
self.cursorBox = BoundingBox(self.cursorBox.xmin, self.cursorBox.ymin, p.x, p.y)
|
385
422
|
self.cursorBox.w = max(self.cursorBox.w, self.canvasCharWidth)
|
386
423
|
self.cursorBox.h = max(self.cursorBox.h, self.canvasCharHeight)
|
387
424
|
|
425
|
+
def setCursorSizeInPlotterPixels(self, w, h):
|
426
|
+
self.setCursorSize(Point(self.cursorBox.xmin + w/2 * self.canvasCharWidth,
|
427
|
+
self.cursorBox.ymin + h/4 * self.canvasCharHeight))
|
428
|
+
|
429
|
+
def formatX(self, v):
|
430
|
+
return str(v)
|
431
|
+
|
432
|
+
def formatY(self, v):
|
433
|
+
return str(v)
|
434
|
+
|
388
435
|
def commandCursor(sheet, execstr):
|
389
436
|
'Return (col, row) of cursor suitable for cmdlog replay of execstr.'
|
390
437
|
contains = lambda s, *substrs: any((a in s) for a in substrs)
|
@@ -425,21 +472,29 @@ class Canvas(Plotter):
|
|
425
472
|
self.scaleX(self.cursorBox.xmax),
|
426
473
|
self.scaleY(self.cursorBox.ymax))
|
427
474
|
|
428
|
-
def
|
475
|
+
def startCursor(self):
|
476
|
+
cm = self.canvasMouse
|
477
|
+
if cm:
|
478
|
+
self.cursorBox = Box(*cm.xy)
|
479
|
+
return True
|
480
|
+
else:
|
481
|
+
return None
|
482
|
+
|
483
|
+
def point(self, x, y, attr:"str|ColorAttr=''", row=None):
|
429
484
|
self.polylines.append(([(x, y)], attr, row))
|
430
485
|
|
431
|
-
def line(self, x1, y1, x2, y2, attr=
|
486
|
+
def line(self, x1, y1, x2, y2, attr:"str|ColorAttr=''", row=None):
|
432
487
|
self.polylines.append(([(x1, y1), (x2, y2)], attr, row))
|
433
488
|
|
434
|
-
def polyline(self, vertexes, attr=
|
489
|
+
def polyline(self, vertexes, attr:"str|ColorAttr=''", row=None):
|
435
490
|
'adds lines for (x,y) vertexes of a polygon'
|
436
491
|
self.polylines.append((vertexes, attr, row))
|
437
492
|
|
438
|
-
def polygon(self, vertexes, attr=
|
493
|
+
def polygon(self, vertexes, attr:"str|ColorAttr=''", row=None):
|
439
494
|
'adds lines for (x,y) vertexes of a polygon'
|
440
495
|
self.polylines.append((vertexes + [vertexes[0]], attr, row))
|
441
496
|
|
442
|
-
def qcurve(self, vertexes, attr=
|
497
|
+
def qcurve(self, vertexes, attr:"str|ColorAttr=''", row=None):
|
443
498
|
'Draw quadratic curve from vertexes[0] to vertexes[2] with control point at vertexes[1]'
|
444
499
|
if len(vertexes) != 3:
|
445
500
|
vd.fail('need exactly 3 points for qcurve (got %d)' % len(vertexes))
|
@@ -451,7 +506,7 @@ class Canvas(Plotter):
|
|
451
506
|
for x, y in bezier(x1, y1, x2, y2, x3, y3):
|
452
507
|
self.point(x, y, attr, row)
|
453
508
|
|
454
|
-
def label(self, x, y, text, attr=
|
509
|
+
def label(self, x, y, text, attr:"str|ColorAttr=''", row=None):
|
455
510
|
self.gridlabels.append((x, y, text, attr, row))
|
456
511
|
|
457
512
|
def fixPoint(self, plotterPoint, canvasPoint):
|
@@ -473,7 +528,7 @@ class Canvas(Plotter):
|
|
473
528
|
self.resetBounds()
|
474
529
|
|
475
530
|
def resetBounds(self):
|
476
|
-
'create canvasBox and cursorBox if necessary, and set visibleBox w/h according to zoomlevels. then redisplay
|
531
|
+
'create canvasBox and cursorBox if necessary, and set visibleBox w/h according to zoomlevels. then redisplay legends.'
|
477
532
|
if not self.canvasBox:
|
478
533
|
xmin, ymin, xmax, ymax = None, None, None, None
|
479
534
|
for vertexes, attr, row in self.polylines:
|
@@ -504,17 +559,33 @@ class Canvas(Plotter):
|
|
504
559
|
self.visibleBox.h = h
|
505
560
|
|
506
561
|
if not self.cursorBox:
|
507
|
-
|
562
|
+
cb_xmin = self.visibleBox.xmin
|
563
|
+
cb_ymin = self.calcBottomCursorY()
|
564
|
+
self.cursorBox = Box(cb_xmin, cb_ymin, self.canvasCharWidth, self.canvasCharHeight)
|
508
565
|
|
509
566
|
self.plotlegends()
|
510
567
|
|
568
|
+
def calcTopCursorY(self):
|
569
|
+
'ymin for the cursor that will align its top with the top edge of the graph'
|
570
|
+
# + (1/4*self.canvasCharHeight) shifts the cursor up by 1 plotter pixel.
|
571
|
+
# That shift makes the cursor contain the top data point.
|
572
|
+
# Otherwise, the top data point would have y == plotterCursorBox.ymax,
|
573
|
+
# which would not be inside plotterCursorBox. Shifting the cursor makes
|
574
|
+
# plotterCursorBox.ymax > y for that top point.
|
575
|
+
return self.visibleBox.ymax - self.cursorBox.h + (1/4*self.canvasCharHeight)
|
576
|
+
|
577
|
+
def calcBottomCursorY(self):
|
578
|
+
'ymin for the cursor that will align its bottom with the bottom edge of the graph'
|
579
|
+
return self.visibleBox.ymin
|
580
|
+
|
511
581
|
def plotlegends(self):
|
512
582
|
# display labels
|
513
583
|
for i, (legend, attr) in enumerate(self.legends.items()):
|
514
|
-
self.addCommand(str(i+1), 'toggle
|
584
|
+
self.addCommand(str(i+1), f'toggle-{i+1}', f'hideAttr("{attr}", "{attr}" not in hiddenAttrs)', f'toggle display of "{legend}"')
|
515
585
|
if attr in self.hiddenAttrs:
|
516
|
-
attr =
|
517
|
-
|
586
|
+
attr = 'graph_hidden'
|
587
|
+
# add 2 characters to width to account for '1:' '2:' etc
|
588
|
+
self.plotlegend(i, '%s:%s'%(i+1,legend), attr, width=self.legendwidth+2)
|
518
589
|
|
519
590
|
def checkCursor(self):
|
520
591
|
'override Sheet.checkCursor'
|
@@ -570,14 +641,21 @@ class Canvas(Plotter):
|
|
570
641
|
else:
|
571
642
|
return h
|
572
643
|
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
644
|
+
def scaleX(self, canvasX):
|
645
|
+
'returns a plotter x coordinate'
|
646
|
+
return round(self.plotviewBox.xmin+(canvasX-self.visibleBox.xmin)*self.xScaler)
|
647
|
+
|
648
|
+
def scaleY(self, canvasY):
|
649
|
+
'returns a plotter y coordinate'
|
650
|
+
return round(self.plotviewBox.ymin+(canvasY-self.visibleBox.ymin)*self.yScaler)
|
651
|
+
|
652
|
+
def unscaleX(self, plotterX):
|
653
|
+
'performs the inverse of scaleX, returns a canvas x coordinate'
|
654
|
+
return (plotterX-self.plotviewBox.xmin)/self.xScaler + self.visibleBox.xmin
|
577
655
|
|
578
|
-
def
|
579
|
-
'returns
|
580
|
-
return
|
656
|
+
def unscaleY(self, plotterY):
|
657
|
+
'performs the inverse of scaleY, returns a canvas y coordinate'
|
658
|
+
return (plotterY-self.plotviewBox.ymin)/self.yScaler + self.visibleBox.ymin
|
581
659
|
|
582
660
|
def canvasW(self, plotter_width):
|
583
661
|
'plotter X units to canvas units'
|
@@ -601,25 +679,33 @@ class Canvas(Plotter):
|
|
601
679
|
|
602
680
|
@asyncthread
|
603
681
|
def render_async(self):
|
604
|
-
self.
|
682
|
+
self.plot_elements()
|
605
683
|
|
606
|
-
def
|
607
|
-
'plots points and lines and text onto the
|
684
|
+
def plot_elements(self, invert_y=False):
|
685
|
+
'plots points and lines and text onto the plotter'
|
608
686
|
|
609
687
|
self.resetBounds()
|
610
688
|
|
611
689
|
bb = self.visibleBox
|
612
690
|
xmin, ymin, xmax, ymax = bb.xmin, bb.ymin, bb.xmax, bb.ymax
|
613
691
|
xfactor, yfactor = self.xScaler, self.yScaler
|
614
|
-
plotxmin
|
692
|
+
plotxmin = self.plotviewBox.xmin
|
693
|
+
if invert_y:
|
694
|
+
plotymax = self.plotviewBox.ymax
|
695
|
+
else:
|
696
|
+
plotymin = self.plotviewBox.ymin
|
615
697
|
|
616
698
|
for vertexes, attr, row in Progress(self.polylines, 'rendering'):
|
617
699
|
if len(vertexes) == 1: # single point
|
618
700
|
x1, y1 = vertexes[0]
|
619
701
|
x1, y1 = float(x1), float(y1)
|
620
702
|
if xmin <= x1 <= xmax and ymin <= y1 <= ymax:
|
703
|
+
# equivalent to self.scaleX(x1) and self.scaleY(y1), inlined for speed
|
621
704
|
x = plotxmin+(x1-xmin)*xfactor
|
622
|
-
|
705
|
+
if invert_y:
|
706
|
+
y = plotymax-(y1-ymin)*yfactor
|
707
|
+
else:
|
708
|
+
y = plotymin+(y1-ymin)*yfactor
|
623
709
|
self.plotpixel(round(x), round(y), attr, row)
|
624
710
|
continue
|
625
711
|
|
@@ -629,9 +715,13 @@ class Canvas(Plotter):
|
|
629
715
|
if r:
|
630
716
|
x1, y1, x2, y2 = r
|
631
717
|
x1 = plotxmin+float(x1-xmin)*xfactor
|
632
|
-
y1 = plotymin+float(y1-ymin)*yfactor
|
633
718
|
x2 = plotxmin+float(x2-xmin)*xfactor
|
634
|
-
|
719
|
+
if invert_y:
|
720
|
+
y1 = plotymax-float(y1-ymin)*yfactor
|
721
|
+
y2 = plotymax-float(y2-ymin)*yfactor
|
722
|
+
else:
|
723
|
+
y1 = plotymin+float(y1-ymin)*yfactor
|
724
|
+
y2 = plotymin+float(y2-ymin)*yfactor
|
635
725
|
self.plotline(x1, y1, x2, y2, attr, row)
|
636
726
|
prev_x, prev_y = x, y
|
637
727
|
|
@@ -646,16 +736,16 @@ class Canvas(Plotter):
|
|
646
736
|
self.reload()
|
647
737
|
|
648
738
|
|
649
|
-
Plotter.addCommand('v', 'visibility', 'options.
|
739
|
+
Plotter.addCommand('v', 'visibility', 'options.disp_graph_labels = not options.disp_graph_labels', 'toggle disp_graph_labels option')
|
650
740
|
|
651
|
-
Canvas.addCommand(None, 'go-left', 'sheet.cursorBox.xmin -= cursorBox.w', 'move cursor left by its width')
|
652
|
-
Canvas.addCommand(None, 'go-right', 'sheet.cursorBox.xmin += cursorBox.w', 'move cursor right by its width' )
|
653
|
-
Canvas.addCommand(None, 'go-up', 'sheet.cursorBox.ymin -= cursorBox.h', 'move cursor up by its height')
|
654
|
-
Canvas.addCommand(None, 'go-down', 'sheet.cursorBox.ymin += cursorBox.h', 'move cursor down by its height')
|
655
|
-
Canvas.addCommand(None, 'go-leftmost', 'sheet.cursorBox.xmin = visibleBox.xmin', 'move cursor to left edge of visible canvas')
|
656
|
-
Canvas.addCommand(None, 'go-rightmost', 'sheet.cursorBox.xmin = visibleBox.xmax-cursorBox.w', 'move cursor to right edge of visible canvas')
|
657
|
-
Canvas.addCommand(None, 'go-top',
|
658
|
-
Canvas.addCommand(None, 'go-bottom', 'sheet.cursorBox.ymin =
|
741
|
+
Canvas.addCommand(None, 'go-left', 'if cursorBox: sheet.cursorBox.xmin -= cursorBox.w', 'move cursor left by its width')
|
742
|
+
Canvas.addCommand(None, 'go-right', 'if cursorBox: sheet.cursorBox.xmin += cursorBox.w', 'move cursor right by its width' )
|
743
|
+
Canvas.addCommand(None, 'go-up', 'if cursorBox: sheet.cursorBox.ymin -= cursorBox.h', 'move cursor up by its height')
|
744
|
+
Canvas.addCommand(None, 'go-down', 'if cursorBox: sheet.cursorBox.ymin += cursorBox.h', 'move cursor down by its height')
|
745
|
+
Canvas.addCommand(None, 'go-leftmost', 'if cursorBox: sheet.cursorBox.xmin = visibleBox.xmin', 'move cursor to left edge of visible canvas')
|
746
|
+
Canvas.addCommand(None, 'go-rightmost', 'if cursorBox: sheet.cursorBox.xmin = visibleBox.xmax-cursorBox.w+(1/2*canvasCharWidth)', 'move cursor to right edge of visible canvas')
|
747
|
+
Canvas.addCommand(None, 'go-top', 'if cursorBox: sheet.cursorBox.ymin = sheet.calcTopCursorY()', 'move cursor to top edge of visible canvas')
|
748
|
+
Canvas.addCommand(None, 'go-bottom', 'if cursorBox: sheet.cursorBox.ymin = sheet.calcBottomCursorY()', 'move cursor to bottom edge of visible canvas')
|
659
749
|
|
660
750
|
Canvas.addCommand(None, 'go-pagedown', 't=(visibleBox.ymax-visibleBox.ymin); sheet.cursorBox.ymin += t; sheet.visibleBox.ymin += t; refresh()', 'move cursor down to next visible page')
|
661
751
|
Canvas.addCommand(None, 'go-pageup', 't=(visibleBox.ymax-visibleBox.ymin); sheet.cursorBox.ymin -= t; sheet.visibleBox.ymin -= t; refresh()', 'move cursor up to previous visible page')
|
@@ -676,20 +766,27 @@ Canvas.addCommand('J', 'resize-cursor-taller', 'sheet.cursorBox.h += canvasCharH
|
|
676
766
|
Canvas.addCommand('K', 'resize-cursor-shorter', 'sheet.cursorBox.h -= canvasCharHeight', 'decrease cursor height by one character')
|
677
767
|
Canvas.addCommand('zz', 'zoom-cursor', 'zoomTo(cursorBox)', 'set visible bounds to cursor')
|
678
768
|
|
679
|
-
Canvas.addCommand('-', 'zoomout-cursor', 'tmp=cursorBox.center; incrZoom(options.
|
680
|
-
Canvas.addCommand('+', 'zoomin-cursor', 'tmp=cursorBox.center; incrZoom(1.0/options.
|
681
|
-
Canvas.addCommand('_', 'zoom-all', 'sheet.canvasBox = None; sheet.visibleBox = None; sheet.xzoomlevel=sheet.yzoomlevel=1.0; refresh()', 'zoom to fit full extent')
|
769
|
+
Canvas.addCommand('-', 'zoomout-cursor', 'tmp=cursorBox.center; incrZoom(options.disp_zoom_incr); fixPoint(plotviewBox.center, tmp)', 'zoom out from cursor center')
|
770
|
+
Canvas.addCommand('+', 'zoomin-cursor', 'tmp=cursorBox.center; incrZoom(1.0/options.disp_zoom_incr); fixPoint(plotviewBox.center, tmp)', 'zoom into cursor center')
|
771
|
+
Canvas.addCommand('_', 'zoom-all', 'sheet.canvasBox = None; sheet.visibleBox = None; sheet.xzoomlevel=sheet.yzoomlevel=1.0; resetBounds(); refresh()', 'zoom to fit full extent')
|
682
772
|
Canvas.addCommand('z_', 'set-aspect', 'sheet.aspectRatio = float(input("aspect ratio=", value=aspectRatio)); refresh()', 'set aspect ratio')
|
683
773
|
|
684
774
|
# set cursor box with left click
|
685
|
-
Canvas.addCommand('BUTTON1_PRESSED', 'start-cursor', '
|
686
|
-
Canvas.addCommand('BUTTON1_RELEASED', 'end-cursor', 'setCursorSize(
|
687
|
-
|
688
|
-
Canvas.
|
689
|
-
Canvas.
|
690
|
-
|
691
|
-
Canvas.addCommand('
|
692
|
-
Canvas.addCommand('
|
775
|
+
Canvas.addCommand('BUTTON1_PRESSED', 'start-cursor', 'startCursor()', 'start cursor box with left mouse button press')
|
776
|
+
Canvas.addCommand('BUTTON1_RELEASED', 'end-cursor', 'cm=canvasMouse; setCursorSize(cm) if cm else None', 'end cursor box with left mouse button release')
|
777
|
+
Canvas.addCommand('BUTTON1_CLICKED', 'remake-cursor', 'startCursor(); cm=canvasMouse; setCursorSize(cm) if cm else None', 'end cursor box with left mouse button release')
|
778
|
+
Canvas.bindkey('BUTTON1_DOUBLE_CLICKED', 'remake-cursor')
|
779
|
+
Canvas.bindkey('BUTTON1_TRIPLE_CLICKED', 'remake-cursor')
|
780
|
+
|
781
|
+
Canvas.addCommand('BUTTON3_PRESSED', 'start-move', 'cm=canvasMouse; sheet.anchorPoint = cm if cm else None', 'mark grid point to move')
|
782
|
+
Canvas.addCommand('BUTTON3_RELEASED', 'end-move', 'fixPoint(plotterMouse, anchorPoint) if anchorPoint else None', 'mark canvas anchor point')
|
783
|
+
# A click does not actually move the canvas, but gives useful UI feedback. It helps users understand that they can do press-drag-release.
|
784
|
+
Canvas.addCommand('BUTTON3_CLICKED', 'move-canvas', '', 'move canvas (in place)')
|
785
|
+
Canvas.bindkey('BUTTON3_DOUBLE_CLICKED', 'move-canvas')
|
786
|
+
Canvas.bindkey('BUTTON3_TRIPLE_CLICKED', 'move-canvas')
|
787
|
+
|
788
|
+
Canvas.addCommand('ScrollUp', 'zoomin-mouse', 'cm=canvasMouse; incrZoom(1.0/options.disp_zoom_incr) if cm else fail("cannot zoom in on unplotted canvas"); fixPoint(plotterMouse, cm)', 'zoom in with scroll wheel')
|
789
|
+
Canvas.addCommand('ScrollDown', 'zoomout-mouse', 'cm=canvasMouse; incrZoom(options.disp_zoom_incr) if cm else fail("cannot zoom out on unplotted canvas"); fixPoint(plotterMouse, cm)', 'zoom out with scroll wheel')
|
693
790
|
|
694
791
|
Canvas.addCommand('s', 'select-cursor', 'source.select(list(rowsWithin(plotterCursorBox)))', 'select rows on source sheet contained within canvas cursor')
|
695
792
|
Canvas.addCommand('t', 'stoggle-cursor', 'source.toggle(list(rowsWithin(plotterCursorBox)))', 'toggle selection of rows on source sheet contained within canvas cursor')
|
@@ -703,7 +800,6 @@ Canvas.addCommand('gu', 'unselect-visible', 'source.unselect(list(rowsWithin(plo
|
|
703
800
|
Canvas.addCommand('g'+ENTER, 'dive-visible', 'vs=copy(source); vs.rows=list(rowsWithin(plotterVisibleBox)); vd.push(vs)', 'open sheet of source rows visible on screen')
|
704
801
|
Canvas.addCommand('gd', 'delete-visible', 'deleteSourceRows(rowsWithin(plotterVisibleBox))', 'delete rows on source sheet visible on screen')
|
705
802
|
|
706
|
-
|
707
803
|
vd.addGlobals({
|
708
804
|
'Canvas': Canvas,
|
709
805
|
'Plotter': Plotter,
|
@@ -711,3 +807,22 @@ vd.addGlobals({
|
|
711
807
|
'Box': Box,
|
712
808
|
'Point': Point,
|
713
809
|
})
|
810
|
+
|
811
|
+
vd.addMenuItems('''
|
812
|
+
Plot > Resize cursor > height > double > resize-cursor-doubleheight
|
813
|
+
Plot > Resize cursor > height > half > resize-cursor-halfheight
|
814
|
+
Plot > Resize cursor > height > shorter > resize-cursor-shorter
|
815
|
+
Plot > Resize cursor > height > taller > resize-cursor-taller
|
816
|
+
Plot > Resize cursor > width > double > resize-cursor-doublewide
|
817
|
+
Plot > Resize cursor > width > half > resize-cursor-halfwide
|
818
|
+
Plot > Resize cursor > width > thinner > resize-cursor-thinner
|
819
|
+
Plot > Resize cursor > width > wider > resize-cursor-wider
|
820
|
+
Plot > Resize graph > X axis > resize-x-input
|
821
|
+
Plot > Resize graph > Y axis > resize-y-input
|
822
|
+
Plot > Resize graph > aspect ratio > set-aspect
|
823
|
+
Plot > Zoom > out > zoomout-cursor
|
824
|
+
Plot > Zoom > in > zoomin-cursor
|
825
|
+
Plot > Zoom > cursor > zoom-all
|
826
|
+
Plot > Dive into cursor > dive-cursor
|
827
|
+
Plot > Delete > under cursor > delete-cursor
|
828
|
+
''')
|
visidata/choose.py
CHANGED
@@ -5,15 +5,15 @@ from visidata import vd, options, VisiData, ListOfDictSheet, ENTER, CompleteKey,
|
|
5
5
|
vd.option('fancy_chooser', False, 'a nicer selection interface for aggregators and jointype')
|
6
6
|
|
7
7
|
@VisiData.api
|
8
|
-
def chooseOne(vd, choices):
|
8
|
+
def chooseOne(vd, choices, type=''):
|
9
9
|
'Return one user-selected key from *choices*.'
|
10
|
-
return vd.choose(choices, 1)
|
10
|
+
return vd.choose(choices, 1, type=type)
|
11
11
|
|
12
12
|
|
13
13
|
@VisiData.api
|
14
|
-
def choose(vd, choices, n=None):
|
14
|
+
def choose(vd, choices, n=None, type=''):
|
15
15
|
'Return a list of 1 to *n* "key" from elements of *choices* (see chooseMany).'
|
16
|
-
ret = vd.chooseMany(choices) or vd.fail('no choice made')
|
16
|
+
ret = vd.chooseMany(choices, type=type) or vd.fail('no choice made')
|
17
17
|
if n and len(ret) > n:
|
18
18
|
vd.fail('can only choose %s' % n)
|
19
19
|
return ret[0] if n==1 else ret
|
@@ -39,7 +39,7 @@ def chooseFancy(vd, choices):
|
|
39
39
|
|
40
40
|
|
41
41
|
@VisiData.api
|
42
|
-
def chooseMany(vd, choices):
|
42
|
+
def chooseMany(vd, choices, type=''):
|
43
43
|
'''Return a list of 1 or more keys from *choices*, which is a list of
|
44
44
|
dicts. Each element dict must have a unique "key", which must be typed
|
45
45
|
directly by the user in non-fancy mode (therefore no spaces). All other
|
@@ -65,7 +65,7 @@ def chooseMany(vd, choices):
|
|
65
65
|
if ret:
|
66
66
|
raise ReturnValue(ret)
|
67
67
|
return v, i
|
68
|
-
chosenstr = vd.input(prompt+': ', completer=CompleteKey(choice_keys), bindings={'^X': throw_fancy})
|
68
|
+
chosenstr = vd.input(prompt+': ', completer=CompleteKey(choice_keys), bindings={'^X': throw_fancy}, type=type)
|
69
69
|
for c in chosenstr.split():
|
70
70
|
if c in choice_keys:
|
71
71
|
chosen.append(c)
|
visidata/clean_names.py
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
import re
|
2
|
+
from visidata import vd, VisiData, Sheet
|
3
|
+
|
4
|
+
|
5
|
+
vd.option('clean_names', False, 'clean column/sheet names to be valid Python identifiers', replay=True)
|
6
|
+
|
7
|
+
|
8
|
+
@VisiData.global_api
|
9
|
+
def cleanName(vd, s):
|
10
|
+
#[Nas Banov] https://stackoverflow.com/a/3305731
|
11
|
+
# return re.sub(r'\W|^(?=\d)', '_', str(s)).strip('_')
|
12
|
+
s = re.sub(r'[^\w\d_]', '_', s) # replace non-alphanum chars with _
|
13
|
+
s = re.sub(r'_+', '_', s) # replace runs of _ with a single _
|
14
|
+
s = s.strip('_')
|
15
|
+
return s
|
16
|
+
|
17
|
+
|
18
|
+
@Sheet.api
|
19
|
+
def maybeClean(sheet, s):
|
20
|
+
if sheet.options.clean_names:
|
21
|
+
s = vd.cleanName(s)
|
22
|
+
return s
|
23
|
+
|
24
|
+
|
25
|
+
Sheet.addCommand('', 'clean-names', '''
|
26
|
+
options.clean_names = True;
|
27
|
+
for c in visibleCols:
|
28
|
+
c.name = cleanName(c.name)
|
29
|
+
''', 'set options.clean_names on sheet and clean visible column names')
|