visidata 2.11.1__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 (255) hide show
  1. visidata/__init__.py +72 -91
  2. visidata/_input.py +259 -42
  3. visidata/_open.py +84 -29
  4. visidata/_types.py +21 -3
  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. {vgit → visidata/apps/vgit}/blame.py +5 -2
  18. {vgit → visidata/apps/vgit}/branch.py +31 -16
  19. {vgit → visidata/apps/vgit}/config.py +3 -3
  20. visidata/apps/vgit/diff.py +169 -0
  21. visidata/apps/vgit/gitsheet.py +161 -0
  22. {vgit → visidata/apps/vgit}/grep.py +6 -5
  23. visidata/apps/vgit/log.py +81 -0
  24. {vgit → visidata/apps/vgit}/main.py +18 -5
  25. {vgit → visidata/apps/vgit}/remote.py +8 -4
  26. visidata/apps/vgit/repos.py +71 -0
  27. {vgit → visidata/apps/vgit}/setup.py +6 -4
  28. visidata/apps/vgit/stash.py +69 -0
  29. visidata/apps/vgit/status.py +204 -0
  30. {vgit → visidata/apps/vgit}/statusbar.py +2 -0
  31. visidata/basesheet.py +59 -50
  32. visidata/canvas.py +208 -93
  33. visidata/choose.py +6 -6
  34. visidata/clean_names.py +29 -0
  35. visidata/clipboard.py +73 -17
  36. visidata/cliptext.py +220 -46
  37. visidata/cmdlog.py +88 -114
  38. visidata/color.py +142 -56
  39. visidata/column.py +121 -129
  40. visidata/ddw/input.ddw +74 -79
  41. visidata/ddw/regex.ddw +57 -0
  42. visidata/ddwplay.py +33 -14
  43. visidata/deprecated.py +77 -3
  44. visidata/desktop/visidata.desktop +7 -0
  45. visidata/editor.py +12 -6
  46. visidata/errors.py +5 -1
  47. visidata/experimental/__init__.py +0 -0
  48. visidata/experimental/diff_sheet.py +29 -0
  49. visidata/experimental/digit_autoedit.py +6 -0
  50. visidata/experimental/gdrive.py +89 -0
  51. visidata/experimental/google.py +37 -0
  52. visidata/experimental/gsheets.py +79 -0
  53. visidata/experimental/live_search.py +37 -0
  54. visidata/experimental/liveupdate.py +45 -0
  55. visidata/experimental/mark.py +133 -0
  56. visidata/experimental/noahs_tapestry/__init__.py +1 -0
  57. visidata/experimental/noahs_tapestry/tapestry.py +147 -0
  58. visidata/experimental/rownum.py +73 -0
  59. visidata/experimental/slide_cells.py +26 -0
  60. visidata/expr.py +8 -4
  61. visidata/extensible.py +30 -5
  62. visidata/features/__init__.py +0 -0
  63. visidata/features/addcol_audiometadata.py +42 -0
  64. visidata/features/addcol_histogram.py +34 -0
  65. visidata/features/canvas_save_svg.py +69 -0
  66. visidata/features/change_precision.py +46 -0
  67. visidata/features/cmdpalette.py +163 -0
  68. visidata/features/colorbrewer.py +363 -0
  69. visidata/{colorsheet.py → features/colorsheet.py} +17 -16
  70. visidata/features/command_server.py +105 -0
  71. visidata/features/currency_to_usd.py +70 -0
  72. visidata/{customdate.py → features/customdate.py} +2 -0
  73. visidata/features/dedupe.py +132 -0
  74. visidata/{describe.py → features/describe.py} +17 -15
  75. visidata/features/errors_guide.py +26 -0
  76. visidata/features/expand_cols.py +202 -0
  77. visidata/{fill.py → features/fill.py} +3 -1
  78. visidata/{freeze.py → features/freeze.py} +11 -6
  79. visidata/features/graph_seaborn.py +79 -0
  80. visidata/features/helloworld.py +10 -0
  81. visidata/features/hint_types.py +17 -0
  82. visidata/{incr.py → features/incr.py} +5 -0
  83. visidata/{join.py → features/join.py} +107 -53
  84. visidata/features/known_cols.py +21 -0
  85. visidata/features/layout.py +62 -0
  86. visidata/{melt.py → features/melt.py} +32 -21
  87. visidata/features/normcol.py +118 -0
  88. visidata/features/open_config.py +7 -0
  89. visidata/features/open_syspaste.py +18 -0
  90. visidata/features/ping.py +157 -0
  91. visidata/features/procmgr.py +208 -0
  92. visidata/features/random_sample.py +6 -0
  93. visidata/{regex.py → features/regex.py} +47 -31
  94. visidata/features/reload_every.py +55 -0
  95. visidata/features/rename_col_cascade.py +30 -0
  96. visidata/features/scroll_context.py +60 -0
  97. visidata/features/select_equal_selected.py +11 -0
  98. visidata/features/setcol_fake.py +65 -0
  99. visidata/{slide.py → features/slide.py} +75 -21
  100. visidata/features/sparkline.py +48 -0
  101. visidata/features/status_source.py +20 -0
  102. visidata/{sysedit.py → features/sysedit.py} +2 -1
  103. visidata/features/sysopen_mailcap.py +46 -0
  104. visidata/features/term_extras.py +13 -0
  105. visidata/{transpose.py → features/transpose.py} +5 -4
  106. visidata/features/type_ipaddr.py +73 -0
  107. visidata/features/type_url.py +11 -0
  108. visidata/{unfurl.py → features/unfurl.py} +9 -9
  109. visidata/{window.py → features/window.py} +2 -2
  110. visidata/form.py +50 -21
  111. visidata/freqtbl.py +81 -33
  112. visidata/fuzzymatch.py +414 -0
  113. visidata/graph.py +105 -33
  114. visidata/guide.py +180 -0
  115. visidata/help.py +75 -44
  116. visidata/hint.py +39 -0
  117. visidata/indexsheet.py +109 -0
  118. visidata/input_history.py +55 -0
  119. visidata/interface.py +58 -0
  120. visidata/keys.py +17 -16
  121. visidata/loaders/__init__.py +9 -0
  122. visidata/loaders/_pandas.py +61 -21
  123. visidata/loaders/api_airtable.py +70 -0
  124. visidata/loaders/api_bitio.py +102 -0
  125. visidata/loaders/api_matrix.py +148 -0
  126. visidata/loaders/api_reddit.py +306 -0
  127. visidata/loaders/api_zulip.py +249 -0
  128. visidata/loaders/archive.py +41 -7
  129. visidata/loaders/arrow.py +7 -7
  130. visidata/loaders/conll.py +49 -0
  131. visidata/loaders/csv.py +25 -7
  132. visidata/loaders/eml.py +3 -4
  133. visidata/loaders/f5log.py +1204 -0
  134. visidata/loaders/fec.py +325 -0
  135. visidata/loaders/fixed_width.py +2 -4
  136. visidata/loaders/frictionless.py +3 -3
  137. visidata/loaders/geojson.py +8 -5
  138. visidata/loaders/google.py +48 -0
  139. visidata/loaders/graphviz.py +4 -4
  140. visidata/loaders/hdf5.py +4 -4
  141. visidata/loaders/html.py +48 -10
  142. visidata/loaders/http.py +84 -30
  143. visidata/loaders/imap.py +20 -10
  144. visidata/loaders/jrnl.py +52 -0
  145. visidata/loaders/json.py +83 -29
  146. visidata/loaders/jsonla.py +74 -0
  147. visidata/loaders/lsv.py +15 -11
  148. visidata/loaders/mailbox.py +40 -0
  149. visidata/loaders/markdown.py +1 -3
  150. visidata/loaders/mbtiles.py +4 -5
  151. visidata/loaders/mysql.py +11 -13
  152. visidata/loaders/npy.py +7 -7
  153. visidata/loaders/odf.py +4 -1
  154. visidata/loaders/orgmode.py +428 -0
  155. visidata/loaders/pandas_freqtbl.py +14 -20
  156. visidata/loaders/parquet.py +62 -6
  157. visidata/loaders/pcap.py +3 -3
  158. visidata/loaders/pdf.py +4 -3
  159. visidata/loaders/png.py +19 -13
  160. visidata/loaders/postgres.py +9 -8
  161. visidata/loaders/rec.py +7 -3
  162. visidata/loaders/s3.py +342 -0
  163. visidata/loaders/sas.py +5 -5
  164. visidata/loaders/scrape.py +186 -0
  165. visidata/loaders/shp.py +6 -5
  166. visidata/loaders/spss.py +5 -6
  167. visidata/loaders/sqlite.py +68 -28
  168. visidata/loaders/texttables.py +1 -1
  169. visidata/loaders/toml.py +60 -0
  170. visidata/loaders/tsv.py +61 -19
  171. visidata/loaders/ttf.py +19 -7
  172. visidata/loaders/unzip_http.py +6 -5
  173. visidata/loaders/usv.py +1 -1
  174. visidata/loaders/vcf.py +16 -16
  175. visidata/loaders/vds.py +10 -7
  176. visidata/loaders/vdx.py +30 -5
  177. visidata/loaders/xlsb.py +8 -1
  178. visidata/loaders/xlsx.py +145 -25
  179. visidata/loaders/xml.py +6 -3
  180. visidata/loaders/xword.py +4 -4
  181. visidata/loaders/yaml.py +15 -5
  182. visidata/macros.py +129 -42
  183. visidata/main.py +119 -94
  184. visidata/mainloop.py +101 -155
  185. visidata/man/parse_options.py +2 -2
  186. visidata/man/vd.1 +301 -148
  187. visidata/man/vd.txt +290 -153
  188. visidata/memory.py +3 -3
  189. visidata/menu.py +104 -423
  190. visidata/metasheets.py +59 -141
  191. visidata/modify.py +78 -23
  192. visidata/motd.py +3 -3
  193. visidata/mouse.py +137 -0
  194. visidata/movement.py +43 -35
  195. visidata/optionssheet.py +99 -0
  196. visidata/path.py +113 -32
  197. visidata/pivot.py +73 -47
  198. visidata/plugins.py +65 -192
  199. visidata/pyobj.py +50 -201
  200. visidata/rename_col.py +20 -0
  201. visidata/save.py +37 -20
  202. visidata/search.py +54 -10
  203. visidata/selection.py +84 -5
  204. visidata/settings.py +162 -25
  205. visidata/sheets.py +229 -257
  206. visidata/shell.py +51 -21
  207. visidata/sidebar.py +162 -0
  208. visidata/sort.py +11 -4
  209. visidata/statusbar.py +113 -104
  210. visidata/stored_list.py +43 -0
  211. visidata/stored_prop.py +38 -0
  212. visidata/tests/conftest.py +3 -3
  213. visidata/tests/test_cliptext.py +39 -0
  214. visidata/tests/test_commands.py +62 -7
  215. visidata/tests/test_edittext.py +2 -2
  216. visidata/tests/test_features.py +17 -0
  217. visidata/tests/test_menu.py +14 -0
  218. visidata/tests/test_path.py +13 -4
  219. visidata/text_source.py +53 -0
  220. visidata/textsheet.py +10 -3
  221. visidata/theme.py +44 -0
  222. visidata/themes/__init__.py +0 -0
  223. visidata/themes/ascii8.py +84 -0
  224. visidata/themes/asciimono.py +84 -0
  225. visidata/themes/light.py +17 -0
  226. visidata/threads.py +87 -39
  227. visidata/tuiwin.py +22 -0
  228. visidata/type_currency.py +22 -3
  229. visidata/type_date.py +31 -9
  230. visidata/type_floatsi.py +5 -1
  231. visidata/undo.py +17 -5
  232. visidata/utils.py +106 -23
  233. visidata/vdobj.py +28 -17
  234. visidata/windows.py +10 -0
  235. visidata/wrappers.py +9 -3
  236. visidata-3.0.data/data/share/applications/visidata.desktop +7 -0
  237. {visidata-2.11.1.data → visidata-3.0.data}/data/share/man/man1/vd.1 +301 -148
  238. {visidata-2.11.1.data → visidata-3.0.data}/data/share/man/man1/visidata.1 +301 -148
  239. visidata-3.0.data/scripts/vd2to3.vdx +9 -0
  240. {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/METADATA +12 -8
  241. visidata-3.0.dist-info/RECORD +257 -0
  242. {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/WHEEL +1 -1
  243. vgit/__init__.py +0 -1
  244. vgit/gitsheet.py +0 -164
  245. visidata/layout.py +0 -44
  246. visidata/misc.py +0 -5
  247. visidata-2.11.1.data/scripts/vgit +0 -9
  248. visidata-2.11.1.dist-info/RECORD +0 -155
  249. {vgit → visidata/apps/vgit}/__main__.py +0 -0
  250. {vgit → visidata/apps/vgit}/abort.py +0 -0
  251. /visidata/{repeat.py → features/repeat.py} +0 -0
  252. {visidata-2.11.1.data → visidata-3.0.data}/scripts/vd +0 -0
  253. {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/LICENSE.gpl3 +0 -0
  254. {visidata-2.11.1.dist-info → visidata-3.0.dist-info}/entry_points.txt +0 -0
  255. {visidata-2.11.1.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:
@@ -504,17 +559,33 @@ class Canvas(Plotter):
504
559
  self.visibleBox.h = h
505
560
 
506
561
  if not self.cursorBox:
507
- 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)
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-%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}"')
515
585
  if attr in self.hiddenAttrs:
516
- attr = colors.color_graph_hidden
517
- 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)
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
- #could be called canvas_to_plotterX()
574
- def scaleX(self, x):
575
- 'returns plotter x coordinate'
576
- return round(self.plotviewBox.xmin+(x-self.visibleBox.xmin)*self.xScaler)
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 scaleY(self, y):
579
- 'returns plotter y coordinate'
580
- return round(self.plotviewBox.ymin+(y-self.visibleBox.ymin)*self.yScaler)
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.render_sync()
682
+ self.plot_elements()
605
683
 
606
- def render_sync(self):
607
- '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'
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, 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
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
- y = plotymin+(y1-ymin)*yfactor
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
- 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
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.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')
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', 'sheet.cursorBox.ymin = visibleBox.ymin', 'move cursor to top edge of visible canvas')
658
- 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')
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.zoom_incr); fixPoint(plotviewBox.center, tmp)', 'zoom out from cursor center')
680
- Canvas.addCommand('+', 'zoomin-cursor', 'tmp=cursorBox.center; incrZoom(1.0/options.zoom_incr); fixPoint(plotviewBox.center, tmp)', 'zoom into cursor center')
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', 'sheet.cursorBox = Box(*canvasMouse.xy)', 'start cursor box with left mouse button press')
686
- Canvas.addCommand('BUTTON1_RELEASED', 'end-cursor', 'setCursorSize(canvasMouse)', 'end cursor box with left mouse button release')
687
-
688
- Canvas.addCommand('BUTTON3_PRESSED', 'start-move', 'sheet.anchorPoint = canvasMouse', 'mark grid point to move')
689
- Canvas.addCommand('BUTTON3_RELEASED', 'end-move', 'fixPoint(plotterMouse, anchorPoint)', 'mark canvas anchor point')
690
-
691
- Canvas.addCommand('ScrollwheelUp', 'zoomin-mouse', 'tmp=canvasMouse; incrZoom(1.0/options.zoom_incr); fixPoint(plotterMouse, tmp)', 'zoom in with scroll wheel')
692
- 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')
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)
@@ -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')