dbt-selly/dbt-env/lib/python3.8/site-packages/agate/table/print_bars.py

250 lines
7.6 KiB
Python

#!/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)