Page Menu
Home
Software Heritage
Search
Configure Global Search
Log In
Files
F8391818
_figure.py
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
15 KB
Subscribers
None
_figure.py
View Options
# -*- coding: utf-8 -*-
from
__future__
import
absolute_import
,
division
,
print_function
,
unicode_literals
# The MIT License
# Copyright (c) 2017 - 2018 Tammo Ippen, tammo.ippen@posteo.de
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from
collections
import
namedtuple
from
datetime
import
timedelta
from
itertools
import
cycle
import
os
from
six.moves
import
zip
from
._canvas
import
Canvas
from
._colors
import
color
from
._input_formatter
import
InputFormatter
from
._util
import
hist
,
mk_timedelta
,
timestamp
# TODO documentation!!!
# TODO tests
class
Figure
(
object
):
"""Figure class to compose multiple plots.
Within a Figure you can easily compose many plots, assign labels to plots
and define the properties of the underlying Canvas. Possible properties that
can be defined are:
width, height: int Define the number of characters in X / Y direction
which are used for plotting.
x_limits: float Define the X limits of the reference coordinate system,
that will be plottered.
y_limits: float Define the Y limits of the reference coordinate system,
that will be plottered.
color_mode: str Define the used color mode. See `plotille.color()`.
with_colors: bool Define, whether to use colors at all.
background: multiple Define the background color.
x_label, y_label: str Define the X / Y axis label.
"""
_COLOR_SEQ
=
[
{
'names'
:
'white'
,
'rgb'
:
(
255
,
255
,
255
),
'byte'
:
0X7
},
{
'names'
:
'red'
,
'rgb'
:
(
255
,
0
,
0
),
'byte'
:
0x1
},
{
'names'
:
'green'
,
'rgb'
:
(
0
,
255
,
0
),
'byte'
:
0x2
},
{
'names'
:
'yellow'
,
'rgb'
:
(
255
,
255
,
0
),
'byte'
:
0x3
},
{
'names'
:
'blue'
,
'rgb'
:
(
0
,
0
,
255
),
'byte'
:
0x4
},
{
'names'
:
'magenta'
,
'rgb'
:
(
255
,
0
,
255
),
'byte'
:
0x5
},
{
'names'
:
'cyan'
,
'rgb'
:
(
0
,
255
,
255
),
'byte'
:
0x6
},
]
def
__init__
(
self
):
self
.
_color_seq
=
iter
(
cycle
(
Figure
.
_COLOR_SEQ
))
self
.
_width
=
None
self
.
_height
=
None
self
.
_x_min
=
None
self
.
_x_max
=
None
self
.
_y_min
=
None
self
.
_y_max
=
None
self
.
_color_mode
=
None
self
.
_with_colors
=
True
self
.
_origin
=
True
self
.
linesep
=
os
.
linesep
self
.
background
=
None
self
.
x_label
=
'X'
self
.
y_label
=
'Y'
self
.
_plots
=
[]
self
.
_in_fmt
=
InputFormatter
()
@property
def
width
(
self
):
if
self
.
_width
is
not
None
:
return
self
.
_width
return
80
@width.setter
def
width
(
self
,
value
):
if
not
(
isinstance
(
value
,
int
)
and
value
>
0
):
raise
ValueError
(
'Invalid width: {}'
.
format
(
value
))
self
.
_width
=
value
@property
def
height
(
self
):
if
self
.
_height
is
not
None
:
return
self
.
_height
return
40
@height.setter
def
height
(
self
,
value
):
if
not
(
isinstance
(
value
,
int
)
and
value
>
0
):
raise
ValueError
(
'Invalid height: {}'
.
format
(
value
))
self
.
_height
=
value
@property
def
color_mode
(
self
):
if
self
.
_color_mode
is
not
None
:
return
self
.
_color_mode
return
'names'
@color_mode.setter
def
color_mode
(
self
,
value
):
if
value
not
in
(
'names'
,
'byte'
,
'rgb'
):
raise
ValueError
(
'Only supports: names, byte, rgb!'
)
if
self
.
_plots
!=
[]:
raise
RuntimeError
(
'Change color mode only, when no plots are prepared.'
)
self
.
_color_mode
=
value
@property
def
with_colors
(
self
):
return
self
.
_with_colors
@with_colors.setter
def
with_colors
(
self
,
value
):
if
not
isinstance
(
value
,
bool
):
raise
ValueError
(
'Only bool allowed: "{}"'
.
format
(
value
))
self
.
_with_colors
=
value
@property
def
origin
(
self
):
return
self
.
_origin
@origin.setter
def
origin
(
self
,
value
):
if
not
isinstance
(
value
,
bool
):
raise
ValueError
(
'Invalid origin: {}'
.
format
(
value
))
self
.
_origin
=
value
def
register_label_formatter
(
self
,
type_
,
formatter
):
self
.
_in_fmt
.
register_formatter
(
type_
,
formatter
)
def
register_float_converter
(
self
,
type_
,
converter
):
self
.
_in_fmt
.
register_converter
(
type_
,
converter
)
def
x_limits
(
self
):
return
self
.
_limits
(
self
.
_x_min
,
self
.
_x_max
,
False
)
def
set_x_limits
(
self
,
min_
=
None
,
max_
=
None
):
self
.
_x_min
,
self
.
_x_max
=
self
.
_set_limits
(
self
.
_x_min
,
self
.
_x_max
,
min_
,
max_
)
def
y_limits
(
self
):
return
self
.
_limits
(
self
.
_y_min
,
self
.
_y_max
,
True
)
def
set_y_limits
(
self
,
min_
=
None
,
max_
=
None
):
self
.
_y_min
,
self
.
_y_max
=
self
.
_set_limits
(
self
.
_y_min
,
self
.
_y_max
,
min_
,
max_
)
def
_set_limits
(
self
,
init_min
,
init_max
,
min_
=
None
,
max_
=
None
):
if
min_
is
not
None
and
max_
is
not
None
:
if
min_
>=
max_
:
raise
ValueError
(
'min_ is larger or equal than max_.'
)
init_min
=
min_
init_max
=
max_
elif
min_
is
not
None
:
if
init_max
is
not
None
and
min_
>=
init_max
:
raise
ValueError
(
'Previous max is smaller or equal to new min_.'
)
init_min
=
min_
elif
max_
is
not
None
:
if
init_min
is
not
None
and
init_min
>=
max_
:
raise
ValueError
(
'Previous min is larger or equal to new max_.'
)
init_max
=
max_
else
:
init_min
=
None
init_max
=
None
return
init_min
,
init_max
def
_limits
(
self
,
low_set
,
high_set
,
is_height
):
if
low_set
is
not
None
and
high_set
is
not
None
:
return
low_set
,
high_set
low
,
high
=
None
,
None
for
p
in
self
.
_plots
:
if
is_height
:
_min
,
_max
=
_limit
(
p
.
height_vals
())
else
:
_min
,
_max
=
_limit
(
p
.
width_vals
())
if
low
is
None
:
low
=
_min
high
=
_max
low
=
min
(
_min
,
low
)
high
=
max
(
_max
,
high
)
return
_choose
(
low
,
high
,
low_set
,
high_set
)
def
_y_axis
(
self
,
ymin
,
ymax
,
label
=
'Y'
):
delta
=
abs
(
ymax
-
ymin
)
if
isinstance
(
delta
,
timedelta
):
y_delta
=
mk_timedelta
(
timestamp
(
delta
)
/
self
.
height
)
else
:
y_delta
=
delta
/
self
.
height
res
=
[
self
.
_in_fmt
.
fmt
(
i
*
y_delta
+
ymin
,
abs
(
ymax
-
ymin
),
chars
=
10
)
+
' | '
for
i
in
range
(
self
.
height
)]
# add max separately
res
+=
[
self
.
_in_fmt
.
fmt
(
self
.
height
*
y_delta
+
ymin
,
abs
(
ymax
-
ymin
),
chars
=
10
)
+
' |'
]
ylbl
=
'({})'
.
format
(
label
)
ylbl_left
=
(
10
-
len
(
ylbl
))
//
2
ylbl_right
=
ylbl_left
+
len
(
ylbl
)
%
2
res
+=
[
' '
*
(
ylbl_left
)
+
ylbl
+
' '
*
(
ylbl_right
)
+
' ^'
]
return
list
(
reversed
(
res
))
def
_x_axis
(
self
,
xmin
,
xmax
,
label
=
'X'
,
with_y_axis
=
False
):
delta
=
abs
(
xmax
-
xmin
)
if
isinstance
(
delta
,
timedelta
):
x_delta
=
mk_timedelta
(
timestamp
(
delta
)
/
self
.
width
)
else
:
x_delta
=
delta
/
self
.
width
starts
=
[
''
,
''
]
if
with_y_axis
:
starts
=
[
'-'
*
11
+
'|-'
,
' '
*
11
+
'| '
]
res
=
[]
res
+=
[
starts
[
0
]
+
'|---------'
*
(
self
.
width
//
10
)
+
'|-> ('
+
label
+
')'
]
res
+=
[
starts
[
1
]
+
' '
.
join
(
self
.
_in_fmt
.
fmt
(
i
*
10
*
x_delta
+
xmin
,
delta
,
left
=
True
,
chars
=
9
)
for
i
in
range
(
self
.
width
//
10
+
1
))]
return
res
def
clear
(
self
):
self
.
_plots
=
[]
def
plot
(
self
,
X
,
Y
,
lc
=
None
,
interp
=
'linear'
,
label
=
None
):
if
len
(
X
)
>
0
:
if
lc
is
None
:
lc
=
next
(
self
.
_color_seq
)[
self
.
color_mode
]
self
.
_plots
+=
[
Plot
.
create
(
X
,
Y
,
lc
,
interp
,
label
)]
def
scatter
(
self
,
X
,
Y
,
lc
=
None
,
label
=
None
):
if
len
(
X
)
>
0
:
if
lc
is
None
:
lc
=
next
(
self
.
_color_seq
)[
self
.
color_mode
]
self
.
_plots
+=
[
Plot
.
create
(
X
,
Y
,
lc
,
None
,
label
)]
def
histogram
(
self
,
X
,
bins
=
160
,
lc
=
None
):
if
len
(
X
)
>
0
:
if
lc
is
None
:
lc
=
next
(
self
.
_color_seq
)[
self
.
color_mode
]
self
.
_plots
+=
[
Histogram
.
create
(
X
,
bins
,
lc
)]
def
show
(
self
,
legend
=
False
):
xmin
,
xmax
=
self
.
x_limits
()
ymin
,
ymax
=
self
.
y_limits
()
if
all
(
isinstance
(
p
,
Histogram
)
for
p
in
self
.
_plots
):
ymin
=
0
# create canvas
canvas
=
Canvas
(
self
.
width
,
self
.
height
,
self
.
_in_fmt
.
convert
(
xmin
),
self
.
_in_fmt
.
convert
(
ymin
),
self
.
_in_fmt
.
convert
(
xmax
),
self
.
_in_fmt
.
convert
(
ymax
),
self
.
background
,
self
.
color_mode
)
plot_origin
=
False
for
p
in
self
.
_plots
:
p
.
write
(
canvas
,
self
.
with_colors
,
self
.
_in_fmt
)
if
isinstance
(
p
,
Plot
):
plot_origin
=
True
if
self
.
origin
and
plot_origin
:
# print X / Y origin axis
canvas
.
line
(
self
.
_in_fmt
.
convert
(
xmin
),
0
,
self
.
_in_fmt
.
convert
(
xmax
),
0
)
canvas
.
line
(
0
,
self
.
_in_fmt
.
convert
(
ymin
),
0
,
self
.
_in_fmt
.
convert
(
ymax
))
res
=
canvas
.
plot
(
linesep
=
self
.
linesep
)
# add y axis
yaxis
=
self
.
_y_axis
(
ymin
,
ymax
,
label
=
self
.
y_label
)
res
=
(
yaxis
[
0
]
+
self
.
linesep
# up arrow
+
yaxis
[
1
]
+
self
.
linesep
# maximum
+
self
.
linesep
.
join
(
lbl
+
line
for
lbl
,
line
in
zip
(
yaxis
[
2
:],
res
.
split
(
self
.
linesep
)))
)
# add x axis
xaxis
=
self
.
_x_axis
(
xmin
,
xmax
,
label
=
self
.
x_label
,
with_y_axis
=
True
)
res
=
(
res
+
self
.
linesep
# plot
+
self
.
linesep
.
join
(
xaxis
)
)
if
legend
:
res
+=
'
\n\n
Legend:
\n
-------
\n
'
res
+=
'
\n
'
.
join
([
color
(
'⠤⠤ {}'
.
format
(
p
.
label
if
p
.
label
is
not
None
else
'Label {}'
.
format
(
i
)),
fg
=
p
.
lc
,
mode
=
self
.
color_mode
,
no_color
=
not
self
.
with_colors
)
for
i
,
p
in
enumerate
(
self
.
_plots
)
if
isinstance
(
p
,
Plot
)
])
return
res
class
Plot
(
namedtuple
(
'Plot'
,
[
'X'
,
'Y'
,
'lc'
,
'interp'
,
'label'
])):
@classmethod
def
create
(
cls
,
X
,
Y
,
lc
,
interp
,
label
):
if
len
(
X
)
!=
len
(
Y
):
raise
ValueError
(
'X and Y dim have to be the same.'
)
if
interp
not
in
(
'linear'
,
None
):
raise
ValueError
(
'Only "linear" and None are allowed values for `interp`.'
)
return
cls
(
X
,
Y
,
lc
,
interp
,
label
)
def
width_vals
(
self
):
return
self
.
X
def
height_vals
(
self
):
return
self
.
Y
def
write
(
self
,
canvas
,
with_colors
,
in_fmt
):
# make point iterators
from_points
=
zip
(
map
(
in_fmt
.
convert
,
self
.
X
),
map
(
in_fmt
.
convert
,
self
.
Y
))
to_points
=
zip
(
map
(
in_fmt
.
convert
,
self
.
X
),
map
(
in_fmt
.
convert
,
self
.
Y
))
# remove first point of to_points
(
x0
,
y0
)
=
next
(
to_points
)
color
=
self
.
lc
if
with_colors
else
None
# print first point
canvas
.
point
(
x0
,
y0
,
color
=
color
)
# plot other points and lines
for
(
x0
,
y0
),
(
x
,
y
)
in
zip
(
from_points
,
to_points
):
canvas
.
point
(
x
,
y
,
color
=
color
)
if
self
.
interp
==
'linear'
:
canvas
.
line
(
x0
,
y0
,
x
,
y
,
color
=
color
)
class
Histogram
(
namedtuple
(
'Histogram'
,
[
'X'
,
'bins'
,
'frequencies'
,
'buckets'
,
'lc'
])):
@classmethod
def
create
(
cls
,
X
,
bins
,
lc
):
frequencies
,
buckets
=
hist
(
X
,
bins
)
return
cls
(
X
,
bins
,
frequencies
,
buckets
,
lc
)
def
width_vals
(
self
):
return
self
.
X
def
height_vals
(
self
):
return
self
.
frequencies
def
write
(
self
,
canvas
,
with_colors
,
in_fmt
):
# how fat will one bar of the histogram be
x_diff
=
(
canvas
.
dots_between
(
in_fmt
.
convert
(
self
.
buckets
[
0
]),
0
,
in_fmt
.
convert
(
self
.
buckets
[
1
]),
0
)[
0
]
or
1
)
bin_size
=
(
in_fmt
.
convert
(
self
.
buckets
[
1
])
-
in_fmt
.
convert
(
self
.
buckets
[
0
]))
/
x_diff
color
=
self
.
lc
if
with_colors
else
None
for
i
in
range
(
self
.
bins
):
# for each bucket
if
self
.
frequencies
[
i
]
>
0
:
for
j
in
range
(
x_diff
):
# print bar
x_
=
in_fmt
.
convert
(
self
.
buckets
[
i
])
+
j
*
bin_size
if
canvas
.
xmin
<=
x_
<=
canvas
.
xmax
:
canvas
.
line
(
x_
,
0
,
x_
,
self
.
frequencies
[
i
],
color
=
color
)
def
_limit
(
values
):
_min
=
0
_max
=
1
if
len
(
values
)
>
0
:
_min
=
min
(
values
)
_max
=
max
(
values
)
return
(
_min
,
_max
)
def
_diff
(
low
,
high
):
if
low
==
high
:
if
low
==
0
:
return
0.5
else
:
return
abs
(
low
*
0.1
)
else
:
delta
=
abs
(
high
-
low
)
if
isinstance
(
delta
,
timedelta
):
return
mk_timedelta
(
timestamp
(
delta
)
*
0.1
)
else
:
return
delta
*
0.1
def
_default
(
low_set
,
high_set
):
if
low_set
is
None
and
high_set
is
None
:
return
0.0
,
1.0
# defaults
if
low_set
is
None
and
high_set
is
not
None
:
if
high_set
<=
0
:
return
high_set
-
1
,
high_set
else
:
return
0.0
,
high_set
if
low_set
is
not
None
and
high_set
is
None
:
if
low_set
>=
1
:
return
low_set
,
low_set
+
1
else
:
return
low_set
,
1.0
# Should never get here! => checked in function before
def
_choose
(
low
,
high
,
low_set
,
high_set
):
no_data
=
low
is
None
and
high
is
None
if
no_data
:
return
_default
(
low_set
,
high_set
)
else
:
# some data
if
low_set
is
None
and
high_set
is
None
:
# no restrictions from user, use low & high
diff
=
_diff
(
low
,
high
)
return
low
-
diff
,
high
+
diff
if
low_set
is
None
and
high_set
is
not
None
:
# user sets high end
if
high_set
<
low
:
# high is smaller than lowest value
return
high_set
-
1
,
high_set
diff
=
_diff
(
low
,
high_set
)
return
low
-
diff
,
high_set
if
low_set
is
not
None
and
high_set
is
None
:
# user sets low end
if
low_set
>
high
:
# low is larger than highest value
return
low_set
,
low_set
+
1
diff
=
_diff
(
low_set
,
high
)
return
low_set
,
high
+
diff
# Should never get here! => checked in function before
File Metadata
Details
Attached
Mime Type
text/x-python
Expires
Jun 4 2025, 6:50 PM (12 w, 2 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3398814
Attached To
rPPL python3-plotille
Event Timeline
Log In to Comment