456 lines
19 KiB
Python
456 lines
19 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
This module implements bar chart renderers.
|
|
"""
|
|
from __future__ import division
|
|
from __future__ import absolute_import
|
|
from __future__ import print_function
|
|
from __future__ import unicode_literals
|
|
from abc import abstractmethod
|
|
|
|
from asciimatics.constants import DOUBLE_LINE, SINGLE_LINE
|
|
from asciimatics.renderers.base import DynamicRenderer
|
|
from asciimatics.screen import Screen
|
|
from asciimatics.utilities import BoxTool
|
|
|
|
|
|
class _BarChartBase(DynamicRenderer):
|
|
#: Constant to indicate no axes should be rendered.
|
|
NONE = 0
|
|
NO_AXIS = 0
|
|
|
|
#: Constant to indicate just the x axis should be rendered.
|
|
X_AXIS = 1
|
|
|
|
#: Constant to indicate just the y axis should be rendered.
|
|
Y_AXIS = 2
|
|
|
|
#: Constant to indicate both axes should be rendered.
|
|
BOTH = 3
|
|
BOTH_AXES = 3
|
|
|
|
def __init__(self, height, width, functions, char="#", colour=Screen.COLOUR_GREEN,
|
|
bg=Screen.COLOUR_BLACK, gradient=None, scale=None, axes=Y_AXIS, intervals=None,
|
|
labels=False, border=True, keys=None, gap=None):
|
|
### See children BarChart and VBarChart for argument descriptions and pydocs
|
|
super(_BarChartBase, self).__init__(height, width)
|
|
self._functions = functions
|
|
self._char = char
|
|
self._colours = [colour] if isinstance(colour, int) else colour
|
|
self._bgs = [bg] if isinstance(bg, int) else bg
|
|
self._scale = scale
|
|
self._axes = axes
|
|
self._intervals = intervals
|
|
self._labels = labels
|
|
self._border = border
|
|
self._keys = keys
|
|
self._gap = gap
|
|
|
|
# Box drawing tool for border, allows user to change the border line style
|
|
self._border_lines = BoxTool(self._canvas.unicode_aware, DOUBLE_LINE) if border else None
|
|
|
|
# Box drawing tool for axes
|
|
self._axes_lines = BoxTool(self._canvas.unicode_aware, SINGLE_LINE)
|
|
|
|
# Normalize the gradient so that it is 3-tuple wide (bg is optional, if not there, set it)
|
|
self._gradient = None
|
|
if gradient:
|
|
self._gradient = []
|
|
for item in gradient:
|
|
if len(item) == 2:
|
|
self._gradient.append((item[0], item[1], Screen.COLOUR_BLACK))
|
|
elif len(item) == 3:
|
|
self._gradient.append(item)
|
|
else:
|
|
raise ValueError("Gradients must be 2-tuple or 3-tuple in size")
|
|
|
|
@abstractmethod
|
|
def _render_now(self):
|
|
pass
|
|
|
|
@property
|
|
def border_style(self):
|
|
"""
|
|
The current drawing style of the border. Possible values are defined in
|
|
:mod:`~asciimatics.constants`:
|
|
|
|
* `ASCII_LINE` -- ASCII safe characters
|
|
* `SINGLE_LINE` -- UNICODE based single line
|
|
* `DOUBLE_LINE` -- UNICODE based double line
|
|
|
|
Note that your canvas must support UNICODE style characters to use them
|
|
"""
|
|
return self._border_lines.style
|
|
|
|
@border_style.setter
|
|
def border_style(self, style):
|
|
if self._border_lines:
|
|
self._border_lines.style = style
|
|
|
|
@property
|
|
def axes_style(self):
|
|
"""
|
|
The current drawing style of the axes. Possible values are defined in
|
|
:mod:`~asciimatics.constants`:
|
|
|
|
* `ASCII_LINE` -- ASCII safe characters
|
|
* `SINGLE_LINE` -- UNICODE based single line
|
|
|
|
Note that your canvas must support UNICODE style characters to use them
|
|
"""
|
|
return self._axes_lines.style
|
|
|
|
@axes_style.setter
|
|
def axes_style(self, style):
|
|
self._axes_lines.style = style
|
|
|
|
def _setup_chart(self):
|
|
"""
|
|
Draws any borders and returns initial height, width, and starting X and Y.
|
|
"""
|
|
# Dimensions for the chart.
|
|
int_h = self._canvas.height
|
|
int_w = self._canvas.width
|
|
start_x = 0
|
|
start_y = 0
|
|
|
|
# Create the box around the chart...
|
|
if self._border:
|
|
draw = self._border_lines.box_top(self._canvas.width)
|
|
self._write(draw, 0, 0)
|
|
for line in range(1, self._canvas.height):
|
|
self._write(self._border_lines.v, 0, line)
|
|
self._write(self._border_lines.v, self._canvas.width - 1, line)
|
|
draw = self._border_lines.box_bottom(self._canvas.width)
|
|
self._write(draw, 0, self._canvas.height - 1)
|
|
int_h -= 4
|
|
int_w -= 6
|
|
start_y += 2
|
|
start_x += 3
|
|
|
|
return int_h, int_w, start_x, start_y
|
|
|
|
|
|
class BarChart(_BarChartBase):
|
|
"""
|
|
Renderer to create a horizontal bar chart using the specified functions as inputs for each
|
|
entry. Can be used to chart distributions or for more graphical effect - e.g. to imitate a
|
|
sound equalizer or a progress indicator.
|
|
"""
|
|
|
|
def __init__(self, height, width, functions, char="#", colour=Screen.COLOUR_GREEN,
|
|
bg=Screen.COLOUR_BLACK, gradient=None, scale=None, axes=_BarChartBase.Y_AXIS,
|
|
intervals=None, labels=False, border=True, keys=None, gap=None):
|
|
"""
|
|
:param height: The max height of the rendered image.
|
|
:param width: The max width of the rendered image.
|
|
:param functions: List of functions to chart.
|
|
:param char: Character to use for the bar. Defaults to '#'
|
|
:param colour: Colour(s) to use for the bars. This can be a single value or list of
|
|
values (to cycle around for each bar). Defaults to green.
|
|
:param bg: Background colour to use for the bars. This can be a single value or list of
|
|
values (to cycle around for each bar). Defaults to black.
|
|
:param gradient: Colour gradient to use for the bars. This is a list of tuple pairs
|
|
specifying a threshold and a colour, or triplets to include a background colour too.
|
|
Defaults to no gradients.
|
|
:param scale: Maximum value for the bars. This is used to scale the function values to
|
|
the maximum space available. Any value over this will be truncated when drawn.
|
|
Defaults to the number of available characters in the chart.
|
|
:param axes: Which axes to draw.
|
|
:param intervals: Units for interval markers on the main axis. Defaults to none.
|
|
:param labels: Whether to draw size indication labels on the x-axis.
|
|
:param border: Whether to draw a border around the chart.
|
|
:param keys: Optional keys to name each bar on the y-axis.
|
|
:param gap: distance between bars. A value of None will auto-calculate (default).
|
|
|
|
If the scale parameter is not specified, the maximum length of the bar is based on the
|
|
available space. A chart with no borders, no axes, no keys or labels will have a bar
|
|
length based solely on the width of the graph.
|
|
|
|
* Borders use 4 characters height and 6 characters width
|
|
* Keys use the width of the widest key plus 1
|
|
* Labels use a height of 1
|
|
* An X_AXIS uses a height of 1
|
|
* A Y_AXIS uses a width of 1
|
|
"""
|
|
# Have to have a call to super as the defaults for the class are different than the parent
|
|
super(BarChart, self).__init__(
|
|
height, width, functions, char, colour, bg, gradient, scale, axes, intervals, labels, border,
|
|
keys, gap)
|
|
|
|
def _render_now(self):
|
|
int_h, int_w, start_x, start_y = self._setup_chart()
|
|
|
|
# Make room for the keys if supplied.
|
|
if self._keys:
|
|
max_key = max([len(x) for x in self._keys])
|
|
key_x = start_x
|
|
int_w -= max_key + 1
|
|
start_x += max_key + 1
|
|
|
|
# Now add the axes - resizing chart space as required...
|
|
if (self._axes & BarChart.X_AXIS) > 0:
|
|
int_h -= 1
|
|
|
|
if (self._axes & BarChart.Y_AXIS) > 0:
|
|
int_w -= 1
|
|
start_x += 1
|
|
|
|
if self._labels:
|
|
int_h -= 1
|
|
|
|
# Use given scale or whatever space is left in the grid
|
|
scale = int_w if self._scale is None else self._scale
|
|
|
|
if self._axes & BarChart.X_AXIS:
|
|
self._write(self._axes_lines.h * int_w, start_x, start_y + int_h)
|
|
if self._axes & BarChart.Y_AXIS:
|
|
for line in range(int_h):
|
|
self._write(self._axes_lines.v, start_x - 1, start_y + line)
|
|
if self._axes & BarChart.BOTH == BarChart.BOTH:
|
|
self._write(self._axes_lines.up_right, start_x - 1, start_y + int_h)
|
|
|
|
if self._labels:
|
|
pos = start_y + int_h
|
|
if self._axes & BarChart.X_AXIS:
|
|
pos += 1
|
|
|
|
self._write("0", start_x, pos)
|
|
text = str(scale)
|
|
self._write(text, start_x + int_w - len(text), pos)
|
|
|
|
# Now add any interval markers if required...
|
|
if self._intervals is not None:
|
|
i = self._intervals
|
|
while i < scale:
|
|
x = start_x + int(i * int_w / scale) - 1
|
|
for line in range(int_h):
|
|
self._write(self._axes_lines.v_inside, x, start_y + line)
|
|
self._write(self._axes_lines.h_up, x, start_y + int_h)
|
|
if self._labels:
|
|
val = str(i)
|
|
self._write(val, x - (len(val) // 2), start_y + int_h + 1)
|
|
i += self._intervals
|
|
|
|
# Allow double-width bars if there's space.
|
|
bar_size = 2 if int_h >= (3 * len(self._functions)) - 1 else 1
|
|
|
|
gap = self._gap
|
|
if self._gap is None:
|
|
gap = 0 if len(self._functions) <= 1 else (int_h - (bar_size * len(
|
|
self._functions))) / (len(self._functions) - 1)
|
|
|
|
# Now add the bars...
|
|
for i, fn in enumerate(self._functions):
|
|
bar_len = int(fn() * int_w / scale)
|
|
y = start_y + (i * bar_size) + int(i * gap)
|
|
|
|
# First draw the key if supplied
|
|
if self._keys:
|
|
key = self._keys[i]
|
|
pos = max_key - len(key)
|
|
self._write(key, key_x + pos, y)
|
|
|
|
# Now draw the bar
|
|
colour = self._colours[i % len(self._colours)]
|
|
bg = self._bgs[i % len(self._bgs)]
|
|
if self._gradient:
|
|
# Colour gradient required - break down into chunks for each colour.
|
|
last = 0
|
|
size = 0
|
|
for threshold, colour, bg in self._gradient:
|
|
value = int(threshold * int_w / scale)
|
|
if value - last > 0:
|
|
# Size to fit the available space
|
|
size = value if bar_len >= value else bar_len
|
|
if size > int_w:
|
|
size = int_w
|
|
for line in range(bar_size):
|
|
self._write(
|
|
self._char * (size - last), start_x + last, y + line, colour, bg=bg)
|
|
|
|
# Stop if we reached the end of the line or the chart
|
|
if bar_len < value or size >= int_w:
|
|
break
|
|
last = value
|
|
else:
|
|
# Solid colour - just write the whole block out.
|
|
for line in range(bar_size):
|
|
self._write(self._char * bar_len, start_x, y + line, colour, bg=bg)
|
|
|
|
return self._plain_image, self._colour_map
|
|
|
|
|
|
class VBarChart(_BarChartBase):
|
|
"""
|
|
Renderer to create a vertical bar chart using the specified functions as inputs for each
|
|
entry. Can be used to chart distributions or for more graphical effect - e.g. to imitate a
|
|
sound equalizer or a progress indicator.
|
|
"""
|
|
|
|
def __init__(self, height, width, functions, char="#", colour=Screen.COLOUR_GREEN,
|
|
bg=Screen.COLOUR_BLACK, gradient=None, scale=None, axes=_BarChartBase.X_AXIS,
|
|
intervals=None, labels=False, border=True, keys=None, gap=None):
|
|
"""
|
|
:param height: The max height of the rendered image.
|
|
:param width: The max width of the rendered image.
|
|
:param functions: List of functions to chart.
|
|
:param char: Character to use for the bar. Defaults to '#'
|
|
:param colour: Colour(s) to use for the bars. This can be a single value or list of
|
|
values (to cycle around for each bar). Defaults to green.
|
|
:param bg: Background colour to use for the bars. This can be a single value or list of
|
|
values (to cycle around for each bar). Defaults to black.
|
|
:param gradient: Colour gradient to use for the bars. This is a list of tuple pairs
|
|
specifying a threshold and a colour, or triplets to include a background colour too.
|
|
Defaults to no gradients.
|
|
:param scale: Maximum value for the bars. This is used to scale the function values to
|
|
the maximum space available. Any value over this will be truncated when drawn.
|
|
Defaults to the number of available characters in the chart.
|
|
:param axes: Which axes to draw.
|
|
:param intervals: Units for interval markers on the main axis. Defaults to none.
|
|
:param labels: Whether to draw size indication labels on the y-axis.
|
|
:param border: Whether to draw a border around the chart.
|
|
:param keys: Optional keys to name each bar on the x-axis.
|
|
:param gap: distance between bars. A value of None will auto-calculate (default). Minimum
|
|
value when auto-calculated is 1, for no gaps specify 0.
|
|
|
|
If the scale parameter is not specified, the maximum length of the bar is based on the
|
|
available space. A chart with no borders, no axes, no keys or labels will have a bar
|
|
height based solely on the width of the graph.
|
|
|
|
* Borders use 4 characters height and 6 characters width
|
|
* Keys use a height of 1
|
|
* Labels vertical bar chart use the width of the widest label plus 1 (label values
|
|
depend on the scale of the chart)
|
|
* An X_AXIS uses a height of 1
|
|
* A Y_AXIS uses a width of 1
|
|
"""
|
|
super(VBarChart, self).__init__(
|
|
height, width, functions, char, colour, bg, gradient, scale, axes, intervals, labels, border,
|
|
keys, gap)
|
|
|
|
def _render_now(self):
|
|
int_h, int_w, start_x, start_y = self._setup_chart()
|
|
|
|
# Make room for the keys if supplied.
|
|
if self._keys:
|
|
int_h -= 1
|
|
|
|
# Now add the axes - resizing chart space as required...
|
|
if self._axes & VBarChart.X_AXIS:
|
|
int_h -= 1
|
|
|
|
if self._axes & VBarChart.Y_AXIS:
|
|
int_w -= 1
|
|
start_x += 1
|
|
|
|
# Use given scale or whatever space is left in the grid
|
|
scale = int_h if self._scale is None else self._scale
|
|
|
|
# Calculate labels and intervals, adjust width based on widest label
|
|
if self._labels:
|
|
labels = [('', False) for x in range(int_h)]
|
|
labels[0] = ('0', False)
|
|
labels[-1] = (str(scale), False)
|
|
if self._intervals:
|
|
next_interval = self._intervals
|
|
for i in range(0, len(labels)):
|
|
value = (i + 1) * scale / int_h
|
|
if value >= next_interval:
|
|
labels[i] = (str(next_interval), True)
|
|
next_interval += self._intervals
|
|
|
|
# Change size based on
|
|
widest_label = max([len(x[0]) for x in labels])
|
|
int_w -= widest_label + 1
|
|
start_x += widest_label + 1
|
|
|
|
if self._axes & VBarChart.X_AXIS:
|
|
self._write(self._axes_lines.h * int_w, start_x, start_y + int_h)
|
|
if self._axes & VBarChart.Y_AXIS:
|
|
for line in range(int_h):
|
|
self._write(self._axes_lines.v, start_x - 1, start_y + line)
|
|
if self._axes & VBarChart.BOTH == BarChart.BOTH:
|
|
self._write(self._axes_lines.up_right, start_x - 1, start_y + int_h)
|
|
|
|
# Draw labels and intervals
|
|
if self._labels:
|
|
y = start_y + int_h - 1
|
|
for label, interval in labels:
|
|
x = start_x - len(label) - 1
|
|
if label != '':
|
|
self._write(label, x, y)
|
|
if interval:
|
|
self._write(self._axes_lines.v_right, start_x - 1, y)
|
|
self._write(self._axes_lines.h_inside * int_w, start_x, y)
|
|
y -= 1
|
|
|
|
# Size bars based on available space
|
|
bar_width = int_w
|
|
gap = 0
|
|
if len(self._functions) > 1:
|
|
if self._gap is None:
|
|
# Evenly size bars and gaps
|
|
bars_and_gaps = 2 * len(self._functions) - 1
|
|
bar_width = int_w // bars_and_gaps
|
|
gap = bar_width
|
|
total_gap_space = gap * (len(self._functions) - 1)
|
|
else:
|
|
# Use given gap size, calculate bar width
|
|
gap = self._gap
|
|
total_gap_space = gap * (len(self._functions) - 1)
|
|
total_bar_space = int_w - total_gap_space
|
|
bar_width = total_bar_space // len(self._functions)
|
|
|
|
if bar_width <= 0:
|
|
raise ValueError(
|
|
"Not enough space. %s bars + %s space for gaps is > your graph width of %s" %
|
|
(len(self._functions), total_gap_space, int_w))
|
|
|
|
# Write keys
|
|
if self._keys:
|
|
y = start_y + int_h
|
|
if self._axes & VBarChart.X_AXIS:
|
|
y += 1
|
|
|
|
x = start_x
|
|
for key in self._keys:
|
|
self._write(key, x, y)
|
|
x += bar_width + gap
|
|
|
|
# Write bars
|
|
values = [fn() for fn in self._functions]
|
|
scale_factor = scale / int_h
|
|
|
|
for pos in range(1, int_h + 1):
|
|
x = start_x
|
|
threshold = pos * scale_factor - (scale_factor / 2)
|
|
|
|
for index, value in enumerate(values):
|
|
colour = self._colours[index % len(self._colours)]
|
|
bg = self._bgs[index % len(self._bgs)]
|
|
if value >= threshold:
|
|
if self._gradient:
|
|
# First gradient is the base colour
|
|
draw_colour = self._gradient[0][1]
|
|
draw_bg = self._gradient[0][2]
|
|
|
|
# Loop through gradients to see if the colour should
|
|
# be incremented to next value
|
|
pos_value = scale * (pos / int_h)
|
|
for gradient in self._gradient[1:]:
|
|
if pos_value >= gradient[0]:
|
|
draw_colour = gradient[1]
|
|
draw_bg = gradient[2]
|
|
else:
|
|
break
|
|
else:
|
|
draw_colour = colour
|
|
draw_bg = bg
|
|
|
|
self._write(self._char * bar_width, x, start_y + int_h - pos, draw_colour, bg=draw_bg)
|
|
|
|
x += bar_width + gap
|
|
|
|
return self._plain_image, self._colour_map
|