aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/vk/utils
diff options
context:
space:
mode:
authorEgor Tensin <Egor.Tensin@gmail.com>2021-05-03 21:24:16 +0300
committerEgor Tensin <Egor.Tensin@gmail.com>2021-05-03 21:24:16 +0300
commit9a724815823e034e384f7a0d2de946ef6e090487 (patch)
treeb030ade43356b7ce50b2a1c50a2e6f940d584b94 /vk/utils
parentREADME: fix badge link (diff)
downloadvk-scripts-9a724815823e034e384f7a0d2de946ef6e090487.tar.gz
vk-scripts-9a724815823e034e384f7a0d2de946ef6e090487.zip
move scripts from bin/ to vk/
This is done in preparation to moving to PyPI. TODO: * update docs, * merge/rename some scripts.
Diffstat (limited to 'vk/utils')
-rw-r--r--vk/utils/__init__.py0
-rw-r--r--vk/utils/bar_chart.py182
-rw-r--r--vk/utils/io.py51
3 files changed, 233 insertions, 0 deletions
diff --git a/vk/utils/__init__.py b/vk/utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/vk/utils/__init__.py
diff --git a/vk/utils/bar_chart.py b/vk/utils/bar_chart.py
new file mode 100644
index 0000000..f051efc
--- /dev/null
+++ b/vk/utils/bar_chart.py
@@ -0,0 +1,182 @@
+# Copyright (c) 2017 Egor Tensin <Egor.Tensin@gmail.com>
+# This file is part of the "VK scripts" project.
+# For details, see https://github.com/egor-tensin/vk-scripts.
+# Distributed under the MIT License.
+
+import matplotlib.pyplot as plt
+from matplotlib import ticker
+import numpy as np
+
+
+class BarChartBuilder:
+ _BAR_HEIGHT = .5
+
+ THICK_BAR_HEIGHT = _BAR_HEIGHT
+ THIN_BAR_HEIGHT = THICK_BAR_HEIGHT / 2
+
+ def __init__(self, labels_align_middle=True):
+ self._fig, self._ax = plt.subplots()
+ self.labels_align_middle = labels_align_middle
+
+ def set_title(self, title):
+ self._ax.set_title(title)
+
+ def _get_categories_axis(self):
+ return self._ax.get_yaxis()
+
+ def _get_values_axis(self):
+ return self._ax.get_xaxis()
+
+ def set_categories_axis_limits(self, start=None, end=None):
+ bottom, top = self._ax.get_ylim()
+ if start is not None:
+ bottom = start
+ if end is not None:
+ top = end
+ self._ax.set_ylim(bottom=bottom, top=top)
+
+ def set_values_axis_limits(self, start=None, end=None):
+ left, right = self._ax.get_xlim()
+ if start is not None:
+ left = start
+ if end is not None:
+ right = end
+ self._ax.set_xlim(left=left, right=right)
+
+ def enable_grid_for_categories(self):
+ self._get_categories_axis().grid()
+
+ def enable_grid_for_values(self):
+ self._get_values_axis().grid()
+
+ def get_categories_labels(self):
+ return self._get_categories_axis().get_ticklabels()
+
+ def get_values_labels(self):
+ return self._get_values_axis().get_ticklabels()
+
+ def hide_categories(self):
+ self._get_categories_axis().set_major_locator(ticker.NullLocator())
+
+ def set_value_label_formatter(self, fn):
+ self._get_values_axis().set_major_formatter(ticker.FuncFormatter(fn))
+
+ def any_values(self):
+ self._get_values_axis().set_major_locator(ticker.AutoLocator())
+
+ def only_integer_values(self):
+ self._get_values_axis().set_major_locator(ticker.MaxNLocator(integer=True))
+
+ @staticmethod
+ def set_property(*args, **kwargs):
+ plt.setp(*args, **kwargs)
+
+ def _set_size(self, inches, dim=0):
+ fig_size = self._fig.get_size_inches()
+ assert len(fig_size) == 2
+ fig_size[dim] = inches
+ self._fig.set_size_inches(fig_size, forward=True)
+
+ def set_width(self, inches):
+ self._set_size(inches)
+
+ def set_height(self, inches):
+ self._set_size(inches, dim=1)
+
+ _DEFAULT_VALUES_AXIS_MAX = 1
+ assert _DEFAULT_VALUES_AXIS_MAX > 0
+
+ def plot_bars(self, categories, values, bar_height=THICK_BAR_HEIGHT):
+ numof_bars = len(categories)
+ inches_per_bar = 2 * bar_height
+ categories_axis_max = inches_per_bar * numof_bars
+
+ if not numof_bars:
+ categories_axis_max += inches_per_bar
+
+ self.set_height(categories_axis_max)
+ self.set_categories_axis_limits(0, categories_axis_max)
+
+ if not numof_bars:
+ self.set_values_axis_limits(0, self._DEFAULT_VALUES_AXIS_MAX)
+ self.hide_categories()
+ return []
+
+ bar_offset = inches_per_bar / 2
+ bar_offsets = inches_per_bar * np.arange(numof_bars) + bar_offset
+
+ if self.labels_align_middle:
+ self._get_categories_axis().set_ticks(bar_offsets)
+ else:
+ self._get_categories_axis().set_ticks(bar_offsets - bar_offset)
+
+ self._get_categories_axis().set_ticklabels(categories)
+
+ bars = self._ax.barh(bar_offsets, values, align='center',
+ height=bar_height)
+
+ if min(values) >= 0:
+ self.set_values_axis_limits(start=0)
+ if np.isclose(max(values), 0.):
+ self.set_values_axis_limits(end=self._DEFAULT_VALUES_AXIS_MAX)
+ elif max(values) < 0:
+ self.set_values_axis_limits(end=0)
+
+ return bars
+
+ @staticmethod
+ def show():
+ plt.show()
+
+ def save(self, path):
+ self._fig.savefig(path, bbox_inches='tight')
+
+
+if __name__ == '__main__':
+ import argparse
+ parser = argparse.ArgumentParser()
+
+ parser.add_argument('--categories', nargs='*', metavar='LABEL',
+ default=[])
+ parser.add_argument('--values', nargs='*', metavar='N',
+ default=[], type=float)
+
+ parser.add_argument('--output', '-o', help='set output file path')
+
+ parser.add_argument('--align-middle', action='store_true',
+ dest='labels_align_middle',
+ help='align labels to the middle of the bars')
+
+ parser.add_argument('--integer-values', action='store_true',
+ dest='only_integer_values')
+ parser.add_argument('--any-values', action='store_false',
+ dest='only_integer_values')
+
+ parser.add_argument('--grid-categories', action='store_true')
+ parser.add_argument('--grid-values', action='store_true')
+
+ args = parser.parse_args()
+
+ if len(args.categories) < len(args.values):
+ parser.error('too many bar values')
+ if len(args.categories) > len(args.values):
+ args.values.extend([0.] * (len(args.categories) - len(args.values)))
+
+ builder = BarChartBuilder(labels_align_middle=args.labels_align_middle)
+
+ if args.only_integer_values:
+ builder.only_integer_values()
+ else:
+ builder.any_values()
+
+ if args.grid_categories:
+ builder.enable_grid_for_categories()
+ if args.grid_values:
+ builder.enable_grid_for_values()
+
+ builder.plot_bars(args.categories, args.values)
+
+ if args.output is None:
+ builder.show()
+ else:
+ builder.save(args.output)
diff --git a/vk/utils/io.py b/vk/utils/io.py
new file mode 100644
index 0000000..bb8eef9
--- /dev/null
+++ b/vk/utils/io.py
@@ -0,0 +1,51 @@
+# Copyright (c) 2017 Egor Tensin <Egor.Tensin@gmail.com>
+# This file is part of the "VK scripts" project.
+# For details, see https://github.com/egor-tensin/vk-scripts.
+# Distributed under the MIT License.
+
+from contextlib import contextmanager
+import csv
+import json
+import sys
+
+
+class FileWriterJSON:
+ def __init__(self, fd=sys.stdout):
+ self._fd = fd
+
+ def write(self, something):
+ self._fd.write(json.dumps(something, indent=3, ensure_ascii=False))
+ self._fd.write('\n')
+
+
+class FileWriterCSV:
+ def __init__(self, fd=sys.stdout):
+ self._writer = csv.writer(fd, lineterminator='\n')
+
+ @staticmethod
+ def _convert_row_old_python(row):
+ if isinstance(row, (list, tuple)):
+ return row
+ return list(row)
+
+ def write_row(self, row):
+ if sys.version_info < (3, 5):
+ row = self._convert_row_old_python(row)
+ self._writer.writerow(row)
+
+
+@contextmanager
+def _open_file(path=None, default=None, **kwargs):
+ if path is None:
+ yield default
+ else:
+ with open(path, **kwargs) as fd:
+ yield fd
+
+
+def open_output_text_file(path=None):
+ return _open_file(path, default=sys.stdout, mode='w', encoding='utf-8')
+
+
+def open_output_binary_file(path=None):
+ return _open_file(path, default=sys.stdout, mode='wb')