#!/usr/bin/env python # -*- coding: utf8 -*- # pylint: disable=W0212 from collections import OrderedDict try: from cdecimal import Decimal except ImportError: # pragma: no cover from decimal import Decimal import sys from babel.numbers import format_decimal import six from agate.aggregations import Min, Max from agate import config from agate.data_types import Number from agate.exceptions import DataTypeError from agate import utils def print_bars(self, label_column_name='group', value_column_name='Count', domain=None, width=120, output=sys.stdout, printable=False): """ Print a text-based bar chart based on this table. :param label_column_name: The column containing the label values. Defaults to :code:`group`, which is the default output of :meth:`.Table.pivot` or :meth:`.Table.bins`. :param value_column_name: The column containing the bar values. Defaults to :code:`Count`, which is the default output of :meth:`.Table.pivot` or :meth:`.Table.bins`. :param domain: A 2-tuple containing the minimum and maximum values for the chart's x-axis. The domain must be large enough to contain all values in the column. :param width: The width, in characters, to use for the bar chart. Defaults to :code:`120`. :param output: A file-like object to print to. Defaults to :code:`sys.stdout`. :param printable: If true, only printable characters will be outputed. """ tick_mark = config.get_option('tick_char') horizontal_line = config.get_option('horizontal_line_char') locale = config.get_option('default_locale') if printable: bar_mark = config.get_option('printable_bar_char') zero_mark = config.get_option('printable_zero_line_char') else: bar_mark = config.get_option('bar_char') zero_mark = config.get_option('zero_line_char') y_label = label_column_name label_column = self._columns[label_column_name] # if not isinstance(label_column.data_type, Text): # raise ValueError('Only Text data is supported for bar chart labels.') x_label = value_column_name value_column = self._columns[value_column_name] if not isinstance(value_column.data_type, Number): raise DataTypeError('Only Number data is supported for bar chart values.') output = output width = width # Format numbers decimal_places = utils.max_precision(value_column) value_formatter = utils.make_number_formatter(decimal_places) formatted_labels = [] for label in label_column: formatted_labels.append(six.text_type(label)) formatted_values = [] for value in value_column: if value is None: formatted_values.append('-') else: formatted_values.append(format_decimal( value, format=value_formatter, locale=locale )) max_label_width = max(max([len(l) for l in formatted_labels]), len(y_label)) max_value_width = max(max([len(v) for v in formatted_values]), len(x_label)) plot_width = width - (max_label_width + max_value_width + 2) min_value = Min(value_column_name).run(self) max_value = Max(value_column_name).run(self) # Calculate dimensions if domain: x_min = Decimal(domain[0]) x_max = Decimal(domain[1]) if min_value < x_min or max_value > x_max: raise ValueError('Column contains values outside specified domain') else: x_min, x_max = utils.round_limits(min_value, max_value) # All positive if x_min >= 0: x_min = Decimal('0') plot_negative_width = 0 zero_line = 0 plot_positive_width = plot_width - 1 # All negative elif x_max <= 0: x_max = Decimal('0') plot_negative_width = plot_width - 1 zero_line = plot_width - 1 plot_positive_width = 0 # Mixed signs else: spread = x_max - x_min negative_portion = (x_min.copy_abs() / spread) # Subtract one for zero line plot_negative_width = int(((plot_width - 1) * negative_portion).to_integral_value()) zero_line = plot_negative_width plot_positive_width = plot_width - (plot_negative_width + 1) def project(value): if value >= 0: return plot_negative_width + int((plot_positive_width * (value / x_max)).to_integral_value()) else: return plot_negative_width - int((plot_negative_width * (value / x_min)).to_integral_value()) # Calculate ticks ticks = OrderedDict() # First tick ticks[0] = x_min ticks[plot_width - 1] = x_max tick_fractions = [Decimal('0.25'), Decimal('0.5'), Decimal('0.75')] # All positive if x_min >= 0: for fraction in tick_fractions: value = x_max * fraction ticks[project(value)] = value # All negative elif x_max <= 0: for fraction in tick_fractions: value = x_min * fraction ticks[project(value)] = value # Mixed signs else: # Zero tick ticks[zero_line] = Decimal('0') # Halfway between min and 0 value = x_min * Decimal('0.5') ticks[project(value)] = value # Halfway between 0 and max value = x_max * Decimal('0.5') ticks[project(value)] = value decimal_places = utils.max_precision(ticks.values()) tick_formatter = utils.make_number_formatter(decimal_places) ticks_formatted = OrderedDict() for k, v in ticks.items(): ticks_formatted[k] = format_decimal( v, format=tick_formatter, locale=locale ) def write(line): output.write(line + '\n') # Chart top top_line = u'%s %s' % (y_label.ljust(max_label_width), x_label.rjust(max_value_width)) write(top_line) # Bars for i, label in enumerate(formatted_labels): value = value_column[i] if value == 0 or value is None: bar_width = 0 elif value > 0: bar_width = project(value) - plot_negative_width elif value < 0: bar_width = plot_negative_width - project(value) label_text = label.ljust(max_label_width) value_text = formatted_values[i].rjust(max_value_width) bar = bar_mark * bar_width if value is not None and value >= 0: gap = (u' ' * plot_negative_width) # All positive if x_min <= 0: bar = gap + zero_mark + bar else: bar = bar + gap + zero_mark else: bar = u' ' * (plot_negative_width - bar_width) + bar # All negative or mixed signs if value is None or x_max > value: bar = bar + zero_mark bar = bar.ljust(plot_width) write('%s %s %s' % (label_text, value_text, bar)) # Axis & ticks axis = horizontal_line * plot_width tick_text = u' ' * width for i, (tick, label) in enumerate(ticks_formatted.items()): # First tick if tick == 0: offset = 0 # Last tick elif tick == plot_width - 1: offset = -(len(label) - 1) else: offset = int(-(len(label) / 2)) pos = (width - plot_width) + tick + offset # Don't print intermediate ticks that would overlap if tick != 0 and tick != plot_width - 1: if tick_text[pos - 1:pos + len(label) + 1] != ' ' * (len(label) + 2): continue tick_text = tick_text[:pos] + label + tick_text[pos + len(label):] axis = axis[:tick] + tick_mark + axis[tick + 1:] write(axis.rjust(width)) write(tick_text)