visidata 2.11.dev0__py3-none-any.whl → 3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- visidata/__init__.py +72 -91
- visidata/_input.py +263 -44
- visidata/_open.py +84 -29
- visidata/_types.py +22 -4
- visidata/_urlcache.py +17 -4
- visidata/aggregators.py +65 -25
- visidata/apps/__init__.py +0 -0
- visidata/apps/vdsql/__about__.py +8 -0
- visidata/apps/vdsql/__init__.py +5 -0
- visidata/apps/vdsql/__main__.py +27 -0
- visidata/apps/vdsql/_ibis.py +748 -0
- visidata/apps/vdsql/bigquery.py +61 -0
- visidata/apps/vdsql/clickhouse.py +53 -0
- visidata/apps/vdsql/setup.py +40 -0
- visidata/apps/vdsql/snowflake.py +67 -0
- visidata/apps/vgit/__init__.py +13 -0
- visidata/apps/vgit/__main__.py +3 -0
- visidata/apps/vgit/abort.py +23 -0
- visidata/apps/vgit/blame.py +76 -0
- visidata/apps/vgit/branch.py +153 -0
- visidata/apps/vgit/config.py +95 -0
- visidata/apps/vgit/diff.py +169 -0
- visidata/apps/vgit/gitsheet.py +161 -0
- visidata/apps/vgit/grep.py +37 -0
- visidata/apps/vgit/log.py +81 -0
- visidata/apps/vgit/main.py +55 -0
- visidata/apps/vgit/remote.py +57 -0
- visidata/apps/vgit/repos.py +71 -0
- visidata/apps/vgit/setup.py +37 -0
- visidata/apps/vgit/stash.py +69 -0
- visidata/apps/vgit/status.py +204 -0
- visidata/apps/vgit/statusbar.py +34 -0
- visidata/basesheet.py +59 -50
- visidata/canvas.py +251 -99
- visidata/choose.py +15 -11
- visidata/clean_names.py +29 -0
- visidata/clipboard.py +84 -18
- visidata/cliptext.py +220 -46
- visidata/cmdlog.py +89 -114
- visidata/color.py +142 -56
- visidata/column.py +134 -131
- visidata/ddw/input.ddw +74 -79
- visidata/ddw/regex.ddw +57 -0
- visidata/ddwplay.py +33 -14
- visidata/deprecated.py +77 -3
- visidata/desktop/visidata.desktop +7 -0
- visidata/editor.py +12 -6
- visidata/errors.py +5 -1
- visidata/experimental/__init__.py +0 -0
- visidata/experimental/diff_sheet.py +29 -0
- visidata/experimental/digit_autoedit.py +6 -0
- visidata/experimental/gdrive.py +89 -0
- visidata/experimental/google.py +37 -0
- visidata/experimental/gsheets.py +79 -0
- visidata/experimental/live_search.py +37 -0
- visidata/experimental/liveupdate.py +45 -0
- visidata/experimental/mark.py +133 -0
- visidata/experimental/noahs_tapestry/__init__.py +1 -0
- visidata/experimental/noahs_tapestry/tapestry.py +147 -0
- visidata/experimental/rownum.py +73 -0
- visidata/experimental/slide_cells.py +26 -0
- visidata/expr.py +8 -4
- visidata/extensible.py +32 -6
- visidata/features/__init__.py +0 -0
- visidata/features/addcol_audiometadata.py +42 -0
- visidata/features/addcol_histogram.py +34 -0
- visidata/features/canvas_save_svg.py +69 -0
- visidata/features/change_precision.py +46 -0
- visidata/features/cmdpalette.py +163 -0
- visidata/features/colorbrewer.py +363 -0
- visidata/{colorsheet.py → features/colorsheet.py} +17 -16
- visidata/features/command_server.py +105 -0
- visidata/features/currency_to_usd.py +70 -0
- visidata/{customdate.py → features/customdate.py} +2 -0
- visidata/features/dedupe.py +132 -0
- visidata/{describe.py → features/describe.py} +17 -15
- visidata/features/errors_guide.py +26 -0
- visidata/features/expand_cols.py +202 -0
- visidata/{fill.py → features/fill.py} +4 -2
- 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} +33 -21
- visidata/features/normcol.py +118 -0
- visidata/features/open_config.py +7 -0
- visidata/features/open_syspaste.py +18 -0
- visidata/features/ping.py +157 -0
- visidata/features/procmgr.py +208 -0
- visidata/features/random_sample.py +6 -0
- visidata/{regex.py → features/regex.py} +47 -31
- visidata/features/reload_every.py +55 -0
- visidata/features/rename_col_cascade.py +30 -0
- visidata/features/scroll_context.py +60 -0
- visidata/features/select_equal_selected.py +11 -0
- visidata/features/setcol_fake.py +65 -0
- visidata/{slide.py → features/slide.py} +75 -21
- visidata/features/sparkline.py +48 -0
- visidata/features/status_source.py +20 -0
- visidata/{sysedit.py → features/sysedit.py} +2 -1
- visidata/features/sysopen_mailcap.py +46 -0
- visidata/features/term_extras.py +13 -0
- visidata/{transpose.py → features/transpose.py} +5 -4
- visidata/features/type_ipaddr.py +73 -0
- visidata/features/type_url.py +11 -0
- visidata/{unfurl.py → features/unfurl.py} +9 -9
- visidata/{window.py → features/window.py} +2 -2
- visidata/form.py +50 -21
- visidata/freqtbl.py +81 -33
- visidata/fuzzymatch.py +414 -0
- visidata/graph.py +105 -33
- visidata/guide.py +180 -0
- visidata/help.py +75 -44
- visidata/hint.py +39 -0
- visidata/indexsheet.py +109 -0
- visidata/input_history.py +55 -0
- visidata/interface.py +58 -0
- visidata/keys.py +17 -16
- visidata/loaders/__init__.py +9 -0
- visidata/loaders/_pandas.py +61 -21
- visidata/loaders/api_airtable.py +70 -0
- visidata/loaders/api_bitio.py +102 -0
- visidata/loaders/api_matrix.py +148 -0
- visidata/loaders/api_reddit.py +306 -0
- visidata/loaders/api_zulip.py +249 -0
- visidata/loaders/archive.py +41 -7
- visidata/loaders/arrow.py +7 -7
- visidata/loaders/conll.py +49 -0
- visidata/loaders/csv.py +25 -7
- visidata/loaders/eml.py +3 -4
- visidata/loaders/f5log.py +1204 -0
- visidata/loaders/fec.py +325 -0
- visidata/loaders/fixed_width.py +3 -5
- visidata/loaders/frictionless.py +3 -3
- visidata/loaders/geojson.py +8 -5
- visidata/loaders/google.py +48 -0
- visidata/loaders/graphviz.py +4 -4
- visidata/loaders/hdf5.py +4 -4
- visidata/loaders/html.py +48 -10
- visidata/loaders/http.py +84 -30
- visidata/loaders/imap.py +20 -10
- visidata/loaders/jrnl.py +52 -0
- visidata/loaders/json.py +83 -29
- visidata/loaders/jsonla.py +74 -0
- visidata/loaders/lsv.py +15 -11
- visidata/loaders/mailbox.py +40 -0
- visidata/loaders/markdown.py +1 -3
- visidata/loaders/mbtiles.py +4 -5
- visidata/loaders/mysql.py +11 -13
- visidata/loaders/npy.py +7 -7
- visidata/loaders/odf.py +4 -1
- visidata/loaders/orgmode.py +428 -0
- visidata/loaders/pandas_freqtbl.py +14 -20
- visidata/loaders/parquet.py +62 -6
- visidata/loaders/pcap.py +3 -3
- visidata/loaders/pdf.py +4 -3
- visidata/loaders/png.py +19 -13
- visidata/loaders/postgres.py +9 -8
- visidata/loaders/rec.py +7 -3
- visidata/loaders/s3.py +342 -0
- visidata/loaders/sas.py +5 -5
- visidata/loaders/scrape.py +186 -0
- visidata/loaders/shp.py +6 -5
- visidata/loaders/spss.py +5 -6
- visidata/loaders/sqlite.py +68 -28
- visidata/loaders/texttables.py +1 -1
- visidata/loaders/toml.py +60 -0
- visidata/loaders/tsv.py +61 -19
- visidata/loaders/ttf.py +19 -7
- visidata/loaders/unzip_http.py +6 -5
- visidata/loaders/usv.py +1 -1
- visidata/loaders/vcf.py +16 -16
- visidata/loaders/vds.py +10 -7
- visidata/loaders/vdx.py +30 -5
- visidata/loaders/xlsb.py +8 -1
- visidata/loaders/xlsx.py +145 -25
- visidata/loaders/xml.py +6 -3
- visidata/loaders/xword.py +4 -4
- visidata/loaders/yaml.py +15 -5
- visidata/macos.py +1 -1
- visidata/macros.py +130 -41
- visidata/main.py +119 -94
- visidata/mainloop.py +101 -154
- visidata/man/parse_options.py +2 -2
- visidata/man/vd.1 +302 -147
- visidata/man/vd.txt +291 -151
- visidata/memory.py +3 -3
- visidata/menu.py +104 -423
- visidata/metasheets.py +59 -141
- visidata/modify.py +79 -23
- visidata/motd.py +3 -3
- visidata/mouse.py +137 -0
- visidata/movement.py +43 -35
- visidata/optionssheet.py +99 -0
- visidata/path.py +131 -43
- visidata/pivot.py +74 -47
- visidata/plugins.py +65 -192
- visidata/pyobj.py +50 -201
- visidata/rename_col.py +20 -0
- visidata/save.py +42 -20
- visidata/search.py +54 -10
- visidata/selection.py +84 -5
- visidata/settings.py +162 -24
- visidata/sheets.py +229 -257
- visidata/shell.py +51 -21
- visidata/sidebar.py +162 -0
- visidata/sort.py +11 -4
- visidata/statusbar.py +113 -104
- visidata/stored_list.py +43 -0
- visidata/stored_prop.py +38 -0
- visidata/tests/conftest.py +3 -3
- visidata/tests/test_cliptext.py +39 -0
- visidata/tests/test_commands.py +62 -7
- visidata/tests/test_edittext.py +2 -2
- visidata/tests/test_features.py +17 -0
- visidata/tests/test_menu.py +14 -0
- visidata/tests/test_path.py +13 -4
- visidata/text_source.py +53 -0
- visidata/textsheet.py +10 -3
- visidata/theme.py +44 -0
- visidata/themes/__init__.py +0 -0
- visidata/themes/ascii8.py +84 -0
- visidata/themes/asciimono.py +84 -0
- visidata/themes/light.py +17 -0
- visidata/threads.py +87 -39
- visidata/tuiwin.py +22 -0
- visidata/type_currency.py +22 -3
- visidata/type_date.py +31 -9
- visidata/type_floatsi.py +5 -1
- visidata/undo.py +18 -6
- visidata/utils.py +106 -23
- visidata/vdobj.py +28 -17
- visidata/windows.py +10 -0
- visidata/wrappers.py +9 -3
- visidata-3.0.data/data/share/applications/visidata.desktop +7 -0
- {visidata-2.11.dev0.data → visidata-3.0.data}/data/share/man/man1/vd.1 +302 -147
- {visidata-2.11.dev0.data → visidata-3.0.data}/data/share/man/man1/visidata.1 +302 -147
- visidata-3.0.data/scripts/vd2to3.vdx +9 -0
- {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/METADATA +13 -11
- visidata-3.0.dist-info/RECORD +257 -0
- {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/WHEEL +1 -1
- {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/entry_points.txt +0 -1
- visidata/layout.py +0 -44
- visidata/misc.py +0 -5
- visidata-2.11.dev0.dist-info/RECORD +0 -142
- /visidata/{repeat.py → features/repeat.py} +0 -0
- {visidata-2.11.dev0.data → visidata-3.0.data}/scripts/vd +0 -0
- {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/LICENSE.gpl3 +0 -0
- {visidata-2.11.dev0.dist-info → visidata-3.0.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:
|
@@ -482,29 +537,55 @@ class Canvas(Plotter):
|
|
482
537
|
if ymin is None or y < ymin: ymin = y
|
483
538
|
if xmax is None or x > xmax: xmax = x
|
484
539
|
if ymax is None or y > ymax: ymax = y
|
485
|
-
|
486
|
-
|
540
|
+
xmin = xmin or 0
|
541
|
+
xmax = xmax or 0
|
542
|
+
ymin = ymin or 0
|
543
|
+
ymax = ymax or 0
|
544
|
+
if xmin == xmax:
|
545
|
+
xmax += 1
|
546
|
+
if ymin == ymax:
|
547
|
+
ymax += 1
|
548
|
+
self.canvasBox = BoundingBox(float(xmin), float(ymin), float(xmax), float(ymax))
|
549
|
+
|
550
|
+
w = self.calcVisibleBoxWidth()
|
551
|
+
h = self.calcVisibleBoxHeight()
|
487
552
|
if not self.visibleBox:
|
488
553
|
# initialize minx/miny, but w/h must be set first to center properly
|
489
|
-
self.visibleBox = Box(0, 0,
|
490
|
-
self.visibleBox.xmin = self.canvasBox.
|
491
|
-
self.visibleBox.ymin = self.canvasBox.
|
554
|
+
self.visibleBox = Box(0, 0, w, h)
|
555
|
+
self.visibleBox.xmin = self.canvasBox.xmin + (self.canvasBox.w / 2) * (1 - self.xzoomlevel)
|
556
|
+
self.visibleBox.ymin = self.canvasBox.ymin + (self.canvasBox.h / 2) * (1 - self.yzoomlevel)
|
492
557
|
else:
|
493
|
-
self.visibleBox.w =
|
494
|
-
self.visibleBox.h =
|
558
|
+
self.visibleBox.w = w
|
559
|
+
self.visibleBox.h = h
|
495
560
|
|
496
561
|
if not self.cursorBox:
|
497
|
-
|
562
|
+
cb_xmin = self.visibleBox.xmin
|
563
|
+
cb_ymin = self.calcBottomCursorY()
|
564
|
+
self.cursorBox = Box(cb_xmin, cb_ymin, self.canvasCharWidth, self.canvasCharHeight)
|
498
565
|
|
499
566
|
self.plotlegends()
|
500
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
|
+
|
501
581
|
def plotlegends(self):
|
502
582
|
# display labels
|
503
583
|
for i, (legend, attr) in enumerate(self.legends.items()):
|
504
|
-
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}"')
|
505
585
|
if attr in self.hiddenAttrs:
|
506
|
-
attr =
|
507
|
-
|
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)
|
508
589
|
|
509
590
|
def checkCursor(self):
|
510
591
|
'override Sheet.checkCursor'
|
@@ -534,13 +615,47 @@ class Canvas(Plotter):
|
|
534
615
|
else:
|
535
616
|
return yratio
|
536
617
|
|
537
|
-
def
|
538
|
-
|
539
|
-
|
618
|
+
def calcVisibleBoxWidth(self):
|
619
|
+
w = self.canvasBox.w * self.xzoomlevel
|
620
|
+
if self.aspectRatio:
|
621
|
+
h = self.canvasBox.h * self.yzoomlevel
|
622
|
+
xratio = self.plotviewBox.w / w
|
623
|
+
yratio = self.plotviewBox.h / h
|
624
|
+
if xratio <= yratio:
|
625
|
+
return w / self.aspectRatio
|
626
|
+
else:
|
627
|
+
return self.plotviewBox.w / (self.aspectRatio * yratio)
|
628
|
+
else:
|
629
|
+
return w
|
540
630
|
|
541
|
-
def
|
542
|
-
|
543
|
-
|
631
|
+
def calcVisibleBoxHeight(self):
|
632
|
+
h = self.canvasBox.h * self.yzoomlevel
|
633
|
+
if self.aspectRatio:
|
634
|
+
w = self.canvasBox.w * self.yzoomlevel
|
635
|
+
xratio = self.plotviewBox.w / w
|
636
|
+
yratio = self.plotviewBox.h / h
|
637
|
+
if xratio < yratio:
|
638
|
+
return self.plotviewBox.h / xratio
|
639
|
+
else:
|
640
|
+
return h
|
641
|
+
else:
|
642
|
+
return h
|
643
|
+
|
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
|
655
|
+
|
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
|
544
659
|
|
545
660
|
def canvasW(self, plotter_width):
|
546
661
|
'plotter X units to canvas units'
|
@@ -564,25 +679,33 @@ class Canvas(Plotter):
|
|
564
679
|
|
565
680
|
@asyncthread
|
566
681
|
def render_async(self):
|
567
|
-
self.
|
682
|
+
self.plot_elements()
|
568
683
|
|
569
|
-
def
|
570
|
-
'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'
|
571
686
|
|
572
687
|
self.resetBounds()
|
573
688
|
|
574
689
|
bb = self.visibleBox
|
575
690
|
xmin, ymin, xmax, ymax = bb.xmin, bb.ymin, bb.xmax, bb.ymax
|
576
691
|
xfactor, yfactor = self.xScaler, self.yScaler
|
577
|
-
plotxmin
|
692
|
+
plotxmin = self.plotviewBox.xmin
|
693
|
+
if invert_y:
|
694
|
+
plotymax = self.plotviewBox.ymax
|
695
|
+
else:
|
696
|
+
plotymin = self.plotviewBox.ymin
|
578
697
|
|
579
698
|
for vertexes, attr, row in Progress(self.polylines, 'rendering'):
|
580
699
|
if len(vertexes) == 1: # single point
|
581
700
|
x1, y1 = vertexes[0]
|
582
701
|
x1, y1 = float(x1), float(y1)
|
583
702
|
if xmin <= x1 <= xmax and ymin <= y1 <= ymax:
|
703
|
+
# equivalent to self.scaleX(x1) and self.scaleY(y1), inlined for speed
|
584
704
|
x = plotxmin+(x1-xmin)*xfactor
|
585
|
-
|
705
|
+
if invert_y:
|
706
|
+
y = plotymax-(y1-ymin)*yfactor
|
707
|
+
else:
|
708
|
+
y = plotymin+(y1-ymin)*yfactor
|
586
709
|
self.plotpixel(round(x), round(y), attr, row)
|
587
710
|
continue
|
588
711
|
|
@@ -592,9 +715,13 @@ class Canvas(Plotter):
|
|
592
715
|
if r:
|
593
716
|
x1, y1, x2, y2 = r
|
594
717
|
x1 = plotxmin+float(x1-xmin)*xfactor
|
595
|
-
y1 = plotymin+float(y1-ymin)*yfactor
|
596
718
|
x2 = plotxmin+float(x2-xmin)*xfactor
|
597
|
-
|
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
|
598
725
|
self.plotline(x1, y1, x2, y2, attr, row)
|
599
726
|
prev_x, prev_y = x, y
|
600
727
|
|
@@ -609,16 +736,16 @@ class Canvas(Plotter):
|
|
609
736
|
self.reload()
|
610
737
|
|
611
738
|
|
612
|
-
Plotter.addCommand('v', 'visibility', 'options.
|
739
|
+
Plotter.addCommand('v', 'visibility', 'options.disp_graph_labels = not options.disp_graph_labels', 'toggle disp_graph_labels option')
|
613
740
|
|
614
|
-
Canvas.addCommand(None, 'go-left', 'sheet.cursorBox.xmin -= cursorBox.w', 'move cursor left by its width')
|
615
|
-
Canvas.addCommand(None, 'go-right', 'sheet.cursorBox.xmin += cursorBox.w', 'move cursor right by its width' )
|
616
|
-
Canvas.addCommand(None, 'go-up', 'sheet.cursorBox.ymin -= cursorBox.h', 'move cursor up by its height')
|
617
|
-
Canvas.addCommand(None, 'go-down', 'sheet.cursorBox.ymin += cursorBox.h', 'move cursor down by its height')
|
618
|
-
Canvas.addCommand(None, 'go-leftmost', 'sheet.cursorBox.xmin = visibleBox.xmin', 'move cursor to left edge of visible canvas')
|
619
|
-
Canvas.addCommand(None, 'go-rightmost', 'sheet.cursorBox.xmin = visibleBox.xmax-cursorBox.w', 'move cursor to right edge of visible canvas')
|
620
|
-
Canvas.addCommand(None, 'go-top',
|
621
|
-
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')
|
622
749
|
|
623
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')
|
624
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')
|
@@ -639,20 +766,27 @@ Canvas.addCommand('J', 'resize-cursor-taller', 'sheet.cursorBox.h += canvasCharH
|
|
639
766
|
Canvas.addCommand('K', 'resize-cursor-shorter', 'sheet.cursorBox.h -= canvasCharHeight', 'decrease cursor height by one character')
|
640
767
|
Canvas.addCommand('zz', 'zoom-cursor', 'zoomTo(cursorBox)', 'set visible bounds to cursor')
|
641
768
|
|
642
|
-
Canvas.addCommand('-', 'zoomout-cursor', 'tmp=cursorBox.center; incrZoom(options.
|
643
|
-
Canvas.addCommand('+', 'zoomin-cursor', 'tmp=cursorBox.center; incrZoom(1.0/options.
|
644
|
-
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')
|
645
772
|
Canvas.addCommand('z_', 'set-aspect', 'sheet.aspectRatio = float(input("aspect ratio=", value=aspectRatio)); refresh()', 'set aspect ratio')
|
646
773
|
|
647
774
|
# set cursor box with left click
|
648
|
-
Canvas.addCommand('BUTTON1_PRESSED', 'start-cursor', '
|
649
|
-
Canvas.addCommand('BUTTON1_RELEASED', 'end-cursor', 'setCursorSize(
|
650
|
-
|
651
|
-
Canvas.
|
652
|
-
Canvas.
|
653
|
-
|
654
|
-
Canvas.addCommand('
|
655
|
-
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')
|
656
790
|
|
657
791
|
Canvas.addCommand('s', 'select-cursor', 'source.select(list(rowsWithin(plotterCursorBox)))', 'select rows on source sheet contained within canvas cursor')
|
658
792
|
Canvas.addCommand('t', 'stoggle-cursor', 'source.toggle(list(rowsWithin(plotterCursorBox)))', 'toggle selection of rows on source sheet contained within canvas cursor')
|
@@ -666,7 +800,6 @@ Canvas.addCommand('gu', 'unselect-visible', 'source.unselect(list(rowsWithin(plo
|
|
666
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')
|
667
801
|
Canvas.addCommand('gd', 'delete-visible', 'deleteSourceRows(rowsWithin(plotterVisibleBox))', 'delete rows on source sheet visible on screen')
|
668
802
|
|
669
|
-
|
670
803
|
vd.addGlobals({
|
671
804
|
'Canvas': Canvas,
|
672
805
|
'Plotter': Plotter,
|
@@ -674,3 +807,22 @@ vd.addGlobals({
|
|
674
807
|
'Box': Box,
|
675
808
|
'Point': Point,
|
676
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,8 +39,13 @@ def chooseFancy(vd, choices):
|
|
39
39
|
|
40
40
|
|
41
41
|
@VisiData.api
|
42
|
-
def chooseMany(vd, choices):
|
43
|
-
'Return a list of 1 or more keys from *choices*, which is a list of
|
42
|
+
def chooseMany(vd, choices, type=''):
|
43
|
+
'''Return a list of 1 or more keys from *choices*, which is a list of
|
44
|
+
dicts. Each element dict must have a unique "key", which must be typed
|
45
|
+
directly by the user in non-fancy mode (therefore no spaces). All other
|
46
|
+
items in the dicts are also shown in fancy chooser mode. Use previous
|
47
|
+
choices from the replay input if available. Add chosen keys
|
48
|
+
(space-separated) to the cmdlog as input for the current command.'''
|
44
49
|
if vd.cmdlog:
|
45
50
|
v = vd.getLastArgs()
|
46
51
|
if v is not None:
|
@@ -60,13 +65,12 @@ def chooseMany(vd, choices):
|
|
60
65
|
if ret:
|
61
66
|
raise ReturnValue(ret)
|
62
67
|
return v, i
|
63
|
-
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)
|
64
69
|
for c in chosenstr.split():
|
65
|
-
|
66
|
-
|
67
|
-
vd.warning('invalid choice "%s"' % c)
|
70
|
+
if c in choice_keys:
|
71
|
+
chosen.append(c)
|
68
72
|
else:
|
69
|
-
|
73
|
+
vd.warning('invalid choice "%s"' % c)
|
70
74
|
except ReturnValue as e:
|
71
75
|
chosen = e.args[0]
|
72
76
|
|