Shofel2_T124_python/venv/lib/python3.10/site-packages/asciimatics/renderers/charts.py

456 lines
19 KiB
Python
Raw Normal View History

2024-05-25 16:45:07 +00:00
# -*- 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