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.
Files changed (253) hide show
  1. visidata/__init__.py +72 -91
  2. visidata/_input.py +263 -44
  3. visidata/_open.py +84 -29
  4. visidata/_types.py +22 -4
  5. visidata/_urlcache.py +17 -4
  6. visidata/aggregators.py +65 -25
  7. visidata/apps/__init__.py +0 -0
  8. visidata/apps/vdsql/__about__.py +8 -0
  9. visidata/apps/vdsql/__init__.py +5 -0
  10. visidata/apps/vdsql/__main__.py +27 -0
  11. visidata/apps/vdsql/_ibis.py +748 -0
  12. visidata/apps/vdsql/bigquery.py +61 -0
  13. visidata/apps/vdsql/clickhouse.py +53 -0
  14. visidata/apps/vdsql/setup.py +40 -0
  15. visidata/apps/vdsql/snowflake.py +67 -0
  16. visidata/apps/vgit/__init__.py +13 -0
  17. visidata/apps/vgit/__main__.py +3 -0
  18. visidata/apps/vgit/abort.py +23 -0
  19. visidata/apps/vgit/blame.py +76 -0
  20. visidata/apps/vgit/branch.py +153 -0
  21. visidata/apps/vgit/config.py +95 -0
  22. visidata/apps/vgit/diff.py +169 -0
  23. visidata/apps/vgit/gitsheet.py +161 -0
  24. visidata/apps/vgit/grep.py +37 -0
  25. visidata/apps/vgit/log.py +81 -0
  26. visidata/apps/vgit/main.py +55 -0
  27. visidata/apps/vgit/remote.py +57 -0
  28. visidata/apps/vgit/repos.py +71 -0
  29. visidata/apps/vgit/setup.py +37 -0
  30. visidata/apps/vgit/stash.py +69 -0
  31. visidata/apps/vgit/status.py +204 -0
  32. visidata/apps/vgit/statusbar.py +34 -0
  33. visidata/basesheet.py +59 -50
  34. visidata/canvas.py +251 -99
  35. visidata/choose.py +15 -11
  36. visidata/clean_names.py +29 -0
  37. visidata/clipboard.py +84 -18
  38. visidata/cliptext.py +220 -46
  39. visidata/cmdlog.py +89 -114
  40. visidata/color.py +142 -56
  41. visidata/column.py +134 -131
  42. visidata/ddw/input.ddw +74 -79
  43. visidata/ddw/regex.ddw +57 -0
  44. visidata/ddwplay.py +33 -14
  45. visidata/deprecated.py +77 -3
  46. visidata/desktop/visidata.desktop +7 -0
  47. visidata/editor.py +12 -6
  48. visidata/errors.py +5 -1
  49. visidata/experimental/__init__.py +0 -0
  50. visidata/experimental/diff_sheet.py +29 -0
  51. visidata/experimental/digit_autoedit.py +6 -0
  52. visidata/experimental/gdrive.py +89 -0
  53. visidata/experimental/google.py +37 -0
  54. visidata/experimental/gsheets.py +79 -0
  55. visidata/experimental/live_search.py +37 -0
  56. visidata/experimental/liveupdate.py +45 -0
  57. visidata/experimental/mark.py +133 -0
  58. visidata/experimental/noahs_tapestry/__init__.py +1 -0
  59. visidata/experimental/noahs_tapestry/tapestry.py +147 -0
  60. visidata/experimental/rownum.py +73 -0
  61. visidata/experimental/slide_cells.py +26 -0
  62. visidata/expr.py +8 -4
  63. visidata/extensible.py +32 -6
  64. visidata/features/__init__.py +0 -0
  65. visidata/features/addcol_audiometadata.py +42 -0
  66. visidata/features/addcol_histogram.py +34 -0
  67. visidata/features/canvas_save_svg.py +69 -0
  68. visidata/features/change_precision.py +46 -0
  69. visidata/features/cmdpalette.py +163 -0
  70. visidata/features/colorbrewer.py +363 -0
  71. visidata/{colorsheet.py → features/colorsheet.py} +17 -16
  72. visidata/features/command_server.py +105 -0
  73. visidata/features/currency_to_usd.py +70 -0
  74. visidata/{customdate.py → features/customdate.py} +2 -0
  75. visidata/features/dedupe.py +132 -0
  76. visidata/{describe.py → features/describe.py} +17 -15
  77. visidata/features/errors_guide.py +26 -0
  78. visidata/features/expand_cols.py +202 -0
  79. visidata/{fill.py → features/fill.py} +4 -2
  80. visidata/{freeze.py → features/freeze.py} +11 -6
  81. visidata/features/graph_seaborn.py +79 -0
  82. visidata/features/helloworld.py +10 -0
  83. visidata/features/hint_types.py +17 -0
  84. visidata/{incr.py → features/incr.py} +5 -0
  85. visidata/{join.py → features/join.py} +107 -53
  86. visidata/features/known_cols.py +21 -0
  87. visidata/features/layout.py +62 -0
  88. visidata/{melt.py → features/melt.py} +33 -21
  89. visidata/features/normcol.py +118 -0
  90. visidata/features/open_config.py +7 -0
  91. visidata/features/open_syspaste.py +18 -0
  92. visidata/features/ping.py +157 -0
  93. visidata/features/procmgr.py +208 -0
  94. visidata/features/random_sample.py +6 -0
  95. visidata/{regex.py → features/regex.py} +47 -31
  96. visidata/features/reload_every.py +55 -0
  97. visidata/features/rename_col_cascade.py +30 -0
  98. visidata/features/scroll_context.py +60 -0
  99. visidata/features/select_equal_selected.py +11 -0
  100. visidata/features/setcol_fake.py +65 -0
  101. visidata/{slide.py → features/slide.py} +75 -21
  102. visidata/features/sparkline.py +48 -0
  103. visidata/features/status_source.py +20 -0
  104. visidata/{sysedit.py → features/sysedit.py} +2 -1
  105. visidata/features/sysopen_mailcap.py +46 -0
  106. visidata/features/term_extras.py +13 -0
  107. visidata/{transpose.py → features/transpose.py} +5 -4
  108. visidata/features/type_ipaddr.py +73 -0
  109. visidata/features/type_url.py +11 -0
  110. visidata/{unfurl.py → features/unfurl.py} +9 -9
  111. visidata/{window.py → features/window.py} +2 -2
  112. visidata/form.py +50 -21
  113. visidata/freqtbl.py +81 -33
  114. visidata/fuzzymatch.py +414 -0
  115. visidata/graph.py +105 -33
  116. visidata/guide.py +180 -0
  117. visidata/help.py +75 -44
  118. visidata/hint.py +39 -0
  119. visidata/indexsheet.py +109 -0
  120. visidata/input_history.py +55 -0
  121. visidata/interface.py +58 -0
  122. visidata/keys.py +17 -16
  123. visidata/loaders/__init__.py +9 -0
  124. visidata/loaders/_pandas.py +61 -21
  125. visidata/loaders/api_airtable.py +70 -0
  126. visidata/loaders/api_bitio.py +102 -0
  127. visidata/loaders/api_matrix.py +148 -0
  128. visidata/loaders/api_reddit.py +306 -0
  129. visidata/loaders/api_zulip.py +249 -0
  130. visidata/loaders/archive.py +41 -7
  131. visidata/loaders/arrow.py +7 -7
  132. visidata/loaders/conll.py +49 -0
  133. visidata/loaders/csv.py +25 -7
  134. visidata/loaders/eml.py +3 -4
  135. visidata/loaders/f5log.py +1204 -0
  136. visidata/loaders/fec.py +325 -0
  137. visidata/loaders/fixed_width.py +3 -5
  138. visidata/loaders/frictionless.py +3 -3
  139. visidata/loaders/geojson.py +8 -5
  140. visidata/loaders/google.py +48 -0
  141. visidata/loaders/graphviz.py +4 -4
  142. visidata/loaders/hdf5.py +4 -4
  143. visidata/loaders/html.py +48 -10
  144. visidata/loaders/http.py +84 -30
  145. visidata/loaders/imap.py +20 -10
  146. visidata/loaders/jrnl.py +52 -0
  147. visidata/loaders/json.py +83 -29
  148. visidata/loaders/jsonla.py +74 -0
  149. visidata/loaders/lsv.py +15 -11
  150. visidata/loaders/mailbox.py +40 -0
  151. visidata/loaders/markdown.py +1 -3
  152. visidata/loaders/mbtiles.py +4 -5
  153. visidata/loaders/mysql.py +11 -13
  154. visidata/loaders/npy.py +7 -7
  155. visidata/loaders/odf.py +4 -1
  156. visidata/loaders/orgmode.py +428 -0
  157. visidata/loaders/pandas_freqtbl.py +14 -20
  158. visidata/loaders/parquet.py +62 -6
  159. visidata/loaders/pcap.py +3 -3
  160. visidata/loaders/pdf.py +4 -3
  161. visidata/loaders/png.py +19 -13
  162. visidata/loaders/postgres.py +9 -8
  163. visidata/loaders/rec.py +7 -3
  164. visidata/loaders/s3.py +342 -0
  165. visidata/loaders/sas.py +5 -5
  166. visidata/loaders/scrape.py +186 -0
  167. visidata/loaders/shp.py +6 -5
  168. visidata/loaders/spss.py +5 -6
  169. visidata/loaders/sqlite.py +68 -28
  170. visidata/loaders/texttables.py +1 -1
  171. visidata/loaders/toml.py +60 -0
  172. visidata/loaders/tsv.py +61 -19
  173. visidata/loaders/ttf.py +19 -7
  174. visidata/loaders/unzip_http.py +6 -5
  175. visidata/loaders/usv.py +1 -1
  176. visidata/loaders/vcf.py +16 -16
  177. visidata/loaders/vds.py +10 -7
  178. visidata/loaders/vdx.py +30 -5
  179. visidata/loaders/xlsb.py +8 -1
  180. visidata/loaders/xlsx.py +145 -25
  181. visidata/loaders/xml.py +6 -3
  182. visidata/loaders/xword.py +4 -4
  183. visidata/loaders/yaml.py +15 -5
  184. visidata/macos.py +1 -1
  185. visidata/macros.py +130 -41
  186. visidata/main.py +119 -94
  187. visidata/mainloop.py +101 -154
  188. visidata/man/parse_options.py +2 -2
  189. visidata/man/vd.1 +302 -147
  190. visidata/man/vd.txt +291 -151
  191. visidata/memory.py +3 -3
  192. visidata/menu.py +104 -423
  193. visidata/metasheets.py +59 -141
  194. visidata/modify.py +79 -23
  195. visidata/motd.py +3 -3
  196. visidata/mouse.py +137 -0
  197. visidata/movement.py +43 -35
  198. visidata/optionssheet.py +99 -0
  199. visidata/path.py +131 -43
  200. visidata/pivot.py +74 -47
  201. visidata/plugins.py +65 -192
  202. visidata/pyobj.py +50 -201
  203. visidata/rename_col.py +20 -0
  204. visidata/save.py +42 -20
  205. visidata/search.py +54 -10
  206. visidata/selection.py +84 -5
  207. visidata/settings.py +162 -24
  208. visidata/sheets.py +229 -257
  209. visidata/shell.py +51 -21
  210. visidata/sidebar.py +162 -0
  211. visidata/sort.py +11 -4
  212. visidata/statusbar.py +113 -104
  213. visidata/stored_list.py +43 -0
  214. visidata/stored_prop.py +38 -0
  215. visidata/tests/conftest.py +3 -3
  216. visidata/tests/test_cliptext.py +39 -0
  217. visidata/tests/test_commands.py +62 -7
  218. visidata/tests/test_edittext.py +2 -2
  219. visidata/tests/test_features.py +17 -0
  220. visidata/tests/test_menu.py +14 -0
  221. visidata/tests/test_path.py +13 -4
  222. visidata/text_source.py +53 -0
  223. visidata/textsheet.py +10 -3
  224. visidata/theme.py +44 -0
  225. visidata/themes/__init__.py +0 -0
  226. visidata/themes/ascii8.py +84 -0
  227. visidata/themes/asciimono.py +84 -0
  228. visidata/themes/light.py +17 -0
  229. visidata/threads.py +87 -39
  230. visidata/tuiwin.py +22 -0
  231. visidata/type_currency.py +22 -3
  232. visidata/type_date.py +31 -9
  233. visidata/type_floatsi.py +5 -1
  234. visidata/undo.py +18 -6
  235. visidata/utils.py +106 -23
  236. visidata/vdobj.py +28 -17
  237. visidata/windows.py +10 -0
  238. visidata/wrappers.py +9 -3
  239. visidata-3.0.data/data/share/applications/visidata.desktop +7 -0
  240. {visidata-2.11.dev0.data → visidata-3.0.data}/data/share/man/man1/vd.1 +302 -147
  241. {visidata-2.11.dev0.data → visidata-3.0.data}/data/share/man/man1/visidata.1 +302 -147
  242. visidata-3.0.data/scripts/vd2to3.vdx +9 -0
  243. {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/METADATA +13 -11
  244. visidata-3.0.dist-info/RECORD +257 -0
  245. {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/WHEEL +1 -1
  246. {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/entry_points.txt +0 -1
  247. visidata/layout.py +0 -44
  248. visidata/misc.py +0 -5
  249. visidata-2.11.dev0.dist-info/RECORD +0 -142
  250. /visidata/{repeat.py → features/repeat.py} +0 -0
  251. {visidata-2.11.dev0.data → visidata-3.0.data}/scripts/vd +0 -0
  252. {visidata-2.11.dev0.dist-info → visidata-3.0.dist-info}/LICENSE.gpl3 +0 -0
  253. {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.option('show_graph_labels', True, 'show axes and legend on graph')
11
- vd.option('plot_colors', 'green red yellow cyan magenta white 38 136 168', 'list of distinct colors to use for plotting distinct objects')
12
- vd.option('disp_canvas_charset', ''.join(chr(0x2800+i) for i in range(256)), 'charset to render 2x4 blocks on canvas')
13
- vd.option('disp_pixel_random', False, 'randomly choose attr from set of pixels instead of most common')
14
- vd.option('zoom_incr', 2.0, 'amount to multiply current zoomlevel when zooming')
15
- vd.option('color_graph_hidden', '238 blue', 'color of legend for hidden attribute')
16
- vd.option('color_graph_selected', 'bold', 'color of selected graph points')
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=0, row=None):
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=0, row=None):
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=0, row=None):
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=0, width=15):
172
- self.plotlabel(self.plotwidth-width*2, i*4, txt, attr)
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 attr at this pixel.'
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 attr at this pixel.'
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, bbox):
213
- 'return list of deduped rows within bbox'
212
+ def rowsWithin(self, plotter_bbox):
213
+ 'return list of deduped rows within plotter_bbox'
214
214
  ret = {}
215
- for y in range(bbox.ymin, min(len(self.pixels), bbox.ymax+1)):
216
- for x in range(bbox.xmin, min(len(self.pixels[y]), bbox.xmax+1)):
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
- attr = Counter(c for c in block_attrs if c).most_common(1)[0][0]
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
- attr = 0
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
- attr = update_attr(ColorAttr(attr, 0, 0, attr), colors.color_current_row).attr
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.show_graph_labels:
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
- clipdraw(scr, char_y, char_x, txt, attr, len(txt))
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), ...], attr, row)
328
- self.gridlabels = [] # list of (grid_x, grid_y, label, attr, row)
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(colors[colorname.translate(str.maketrans('_', ' '))] for colorname in self.options.plot_colors.split())
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, len(legend))
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
- self.plotviewBox = BoundingBox(self.leftMarginPixels, self.topMarginPixels,
366
- self.plotwidth-self.rightMarginPixels, self.plotheight-self.bottomMarginPixels-1)
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
- return self.canvasFromPlotterCoord(self.plotterMouse.x, self.plotterMouse.y)
375
-
376
- def canvasFromPlotterCoord(self, plotter_x, plotter_y):
377
- return Point(self.visibleBox.xmin + (plotter_x-self.plotviewBox.xmin)/self.xScaler, self.visibleBox.ymin + (plotter_y-self.plotviewBox.ymin)/self.yScaler)
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 point(self, x, y, attr=0, row=None):
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=0, row=None):
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=0, row=None):
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=0, row=None):
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=0, row=None):
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=0, row=None):
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 labels.'
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
- self.canvasBox = BoundingBox(float(xmin or 0), float(ymin or 0), float(xmax or 1), float(ymax or 1))
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, self.plotviewBox.w/self.xScaler, self.plotviewBox.h/self.yScaler)
490
- self.visibleBox.xmin = self.canvasBox.xcenter - self.visibleBox.w/2
491
- self.visibleBox.ymin = self.canvasBox.ycenter - self.visibleBox.h/2
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 = self.plotviewBox.w/self.xScaler
494
- self.visibleBox.h = self.plotviewBox.h/self.yScaler
558
+ self.visibleBox.w = w
559
+ self.visibleBox.h = h
495
560
 
496
561
  if not self.cursorBox:
497
- self.cursorBox = Box(self.visibleBox.xmin, self.visibleBox.ymin, self.canvasCharWidth, self.canvasCharHeight)
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-%s'%(i+1), 'hideAttr(%s, %s not in hiddenAttrs)' % (attr, attr), 'toggle display of "%s"' % legend)
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 = colors.color_graph_hidden
507
- self.plotlegend(i, '%s:%s'%(i+1,legend), attr, width=self.legendwidth+4)
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 scaleX(self, x):
538
- 'returns plotter x coordinate'
539
- return round(self.plotviewBox.xmin+(x-self.visibleBox.xmin)*self.xScaler)
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 scaleY(self, y):
542
- 'returns plotter y coordinate'
543
- return round(self.plotviewBox.ymin+(y-self.visibleBox.ymin)*self.yScaler)
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.render_sync()
682
+ self.plot_elements()
568
683
 
569
- def render_sync(self):
570
- 'plots points and lines and text onto the Plotter'
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, plotymin = self.plotviewBox.xmin, self.plotviewBox.ymin
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
- y = plotymin+(y1-ymin)*yfactor
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
- y2 = plotymin+float(y2-ymin)*yfactor
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.show_graph_labels = not options.show_graph_labels', 'toggle show_graph_labels option')
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', 'sheet.cursorBox.ymin = visibleBox.ymin', 'move cursor to top edge of visible canvas')
621
- Canvas.addCommand(None, 'go-bottom', 'sheet.cursorBox.ymin = visibleBox.ymax', 'move cursor to bottom edge of visible canvas')
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.zoom_incr); fixPoint(plotviewBox.center, tmp)', 'zoom out from cursor center')
643
- Canvas.addCommand('+', 'zoomin-cursor', 'tmp=cursorBox.center; incrZoom(1.0/options.zoom_incr); fixPoint(plotviewBox.center, tmp)', 'zoom into cursor center')
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', 'sheet.cursorBox = Box(*canvasMouse.xy)', 'start cursor box with left mouse button press')
649
- Canvas.addCommand('BUTTON1_RELEASED', 'end-cursor', 'setCursorSize(canvasMouse)', 'end cursor box with left mouse button release')
650
-
651
- Canvas.addCommand('BUTTON3_PRESSED', 'start-move', 'sheet.anchorPoint = canvasMouse', 'mark grid point to move')
652
- Canvas.addCommand('BUTTON3_RELEASED', 'end-move', 'fixPoint(plotterMouse, anchorPoint)', 'mark canvas anchor point')
653
-
654
- Canvas.addCommand('ScrollwheelUp', 'zoomin-mouse', 'tmp=canvasMouse; incrZoom(1.0/options.zoom_incr); fixPoint(plotterMouse, tmp)', 'zoom in with scroll wheel')
655
- Canvas.addCommand('ScrollwheelDown', 'zoomout-mouse', 'tmp=canvasMouse; incrZoom(options.zoom_incr); fixPoint(plotterMouse, tmp)', 'zoom out with scroll wheel')
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 dicts. Each element dict must have a unique "key", which must be typed directly by the user in non-fancy mode (therefore no spaces). All other items in the dicts are also shown in fancy chooser mode. Use previous choices from the replay input if available. Add chosen keys (space-separated) to the cmdlog as input for the current command.'''
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
- poss = [p for p in choice_keys if str(p).startswith(c)]
66
- if not poss:
67
- vd.warning('invalid choice "%s"' % c)
70
+ if c in choice_keys:
71
+ chosen.append(c)
68
72
  else:
69
- chosen.extend(poss)
73
+ vd.warning('invalid choice "%s"' % c)
70
74
  except ReturnValue as e:
71
75
  chosen = e.args[0]
72
76