densitty 0.8.2__py3-none-any.whl → 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- densitty/__init__.py +9 -0
- densitty/ansi.py +0 -1
- densitty/axis.py +222 -59
- densitty/binning.py +200 -115
- densitty/colorbar.py +84 -0
- densitty/detect.py +142 -24
- densitty/{plot.py → plotting.py} +77 -19
- densitty/smoothing.py +315 -0
- densitty/truecolor.py +2 -1
- densitty/util.py +149 -124
- densitty/util.pyi +38 -0
- {densitty-0.8.2.dist-info → densitty-1.0.0.dist-info}/METADATA +11 -6
- densitty-1.0.0.dist-info/RECORD +18 -0
- {densitty-0.8.2.dist-info → densitty-1.0.0.dist-info}/WHEEL +1 -1
- densitty-0.8.2.dist-info/RECORD +0 -15
- {densitty-0.8.2.dist-info → densitty-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {densitty-0.8.2.dist-info → densitty-1.0.0.dist-info}/top_level.txt +0 -0
densitty/detect.py
CHANGED
|
@@ -9,14 +9,14 @@ from types import MappingProxyType
|
|
|
9
9
|
from typing import Any, Callable, Optional, Sequence
|
|
10
10
|
import time
|
|
11
11
|
|
|
12
|
-
from . import ansi, ascii_art, binning, lineart, truecolor
|
|
13
|
-
from . import
|
|
14
|
-
from .util import FloatLike, ValueRange
|
|
12
|
+
from . import ansi, ascii_art, axis, binning, colorbar, lineart, plotting, smoothing, truecolor
|
|
13
|
+
from .util import FloatLike, ValueRange, make_value_range
|
|
15
14
|
|
|
16
15
|
if sys.platform == "win32":
|
|
17
16
|
# pylint: disable=import-error
|
|
18
17
|
import ctypes
|
|
19
|
-
|
|
18
|
+
|
|
19
|
+
kernel32 = ctypes.windll.kernel32
|
|
20
20
|
else:
|
|
21
21
|
# All other platforms should have TERMIOS available
|
|
22
22
|
import fcntl
|
|
@@ -86,10 +86,10 @@ if sys.platform == "win32":
|
|
|
86
86
|
"""Windows-based wrapper to avoid control code output to stdout"""
|
|
87
87
|
prev_stdin_mode = ctypes.wintypes.DWORD(0)
|
|
88
88
|
prev_stdout_mode = ctypes.wintypes.DWORD(0)
|
|
89
|
-
GetConsoleMode(GetStdHandle(-10), ctypes.byref(prev_stdin_mode))
|
|
90
|
-
SetConsoleMode(GetStdHandle(-10), 0)
|
|
91
|
-
GetConsoleMode(GetStdHandle(-11), ctypes.byref(prev_stdout_mode))
|
|
92
|
-
SetConsoleMode(GetStdHandle(-11), 7)
|
|
89
|
+
kernel32.GetConsoleMode(kernel32.GetStdHandle(-10), ctypes.byref(prev_stdin_mode))
|
|
90
|
+
kernel32.SetConsoleMode(kernel32.GetStdHandle(-10), 0)
|
|
91
|
+
kernel32.GetConsoleMode(kernel32.GetStdHandle(-11), ctypes.byref(prev_stdout_mode))
|
|
92
|
+
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
|
|
93
93
|
|
|
94
94
|
# On Windows, don't try to be non-blocking, just read until terminator
|
|
95
95
|
if length is None:
|
|
@@ -106,8 +106,8 @@ if sys.platform == "win32":
|
|
|
106
106
|
break
|
|
107
107
|
return response
|
|
108
108
|
finally:
|
|
109
|
-
SetConsoleMode(GetStdHandle(-10), ctypes.byref(prev_stdin_mode))
|
|
110
|
-
SetConsoleMode(GetStdHandle(-11), ctypes.byref(prev_stdout_mode))
|
|
109
|
+
kernel32.SetConsoleMode(kernel32.GetStdHandle(-10), ctypes.byref(prev_stdin_mode))
|
|
110
|
+
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), ctypes.byref(prev_stdout_mode))
|
|
111
111
|
|
|
112
112
|
else:
|
|
113
113
|
# Not Windows, so use termios/fcntl:
|
|
@@ -304,7 +304,8 @@ def color_support(interactive=True, debug=False) -> ColorSupport:
|
|
|
304
304
|
return ColorSupport.NONE
|
|
305
305
|
|
|
306
306
|
env_term = os.environ.get("TERM", "")
|
|
307
|
-
|
|
307
|
+
if debug:
|
|
308
|
+
print(f"$TERM is {env_term}")
|
|
308
309
|
|
|
309
310
|
truecolor_terminals = ("truecolor", "xterm-kitty", "xterm-ghostty", "wezterm")
|
|
310
311
|
if env_term in truecolor_terminals:
|
|
@@ -362,6 +363,13 @@ def color_support(interactive=True, debug=False) -> ColorSupport:
|
|
|
362
363
|
return min(ColorSupport.ANSI_24BIT, color_cap)
|
|
363
364
|
|
|
364
365
|
if curses:
|
|
366
|
+
# Curses is installed, but it may or may not have been set up. Try and see
|
|
367
|
+
try:
|
|
368
|
+
curses.tigetflag("RGB")
|
|
369
|
+
# If this gets an error, it can be an internal type not derived from Exception
|
|
370
|
+
# so just catch everything:
|
|
371
|
+
except: # pylint: disable=bare-except
|
|
372
|
+
curses.setupterm()
|
|
365
373
|
if curses.tigetflag("RGB") == 1:
|
|
366
374
|
# ncurses 6.0+ / terminfo added an "RGB" capability to indicate truecolor support
|
|
367
375
|
return min(ColorSupport.ANSI_24BIT, color_cap)
|
|
@@ -371,6 +379,8 @@ def color_support(interactive=True, debug=False) -> ColorSupport:
|
|
|
371
379
|
if curses.tigetnum("colors") == 256:
|
|
372
380
|
return min(ColorSupport.ANSI_8BIT, color_cap)
|
|
373
381
|
|
|
382
|
+
curses_colors = curses.tigetnum("colors") # for use below
|
|
383
|
+
|
|
374
384
|
if env_term.endswith("-256color") or env_term.endswith("-256"):
|
|
375
385
|
if debug:
|
|
376
386
|
print("Color detect: $TERM suffix in 8b list")
|
|
@@ -391,8 +401,9 @@ def color_support(interactive=True, debug=False) -> ColorSupport:
|
|
|
391
401
|
print("Color detect: using terminal's Device Attributes")
|
|
392
402
|
return da1_color_support(debug)
|
|
393
403
|
|
|
394
|
-
if curses
|
|
395
|
-
|
|
404
|
+
if curses:
|
|
405
|
+
if curses_colors >= 16:
|
|
406
|
+
return min(ColorSupport.ANSI_4BIT, color_cap)
|
|
396
407
|
|
|
397
408
|
return ColorSupport.NONE
|
|
398
409
|
|
|
@@ -415,6 +426,24 @@ FADE_IN = MappingProxyType(
|
|
|
415
426
|
}
|
|
416
427
|
)
|
|
417
428
|
|
|
429
|
+
RAINBOW = MappingProxyType(
|
|
430
|
+
{
|
|
431
|
+
ColorSupport.NONE: ascii_art.EXTENDED,
|
|
432
|
+
ColorSupport.ANSI_4BIT: ansi.RAINBOW_16,
|
|
433
|
+
ColorSupport.ANSI_8BIT: ansi.RAINBOW,
|
|
434
|
+
ColorSupport.ANSI_24BIT: truecolor.RAINBOW,
|
|
435
|
+
}
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
REV_RAINBOW = MappingProxyType(
|
|
439
|
+
{
|
|
440
|
+
ColorSupport.NONE: ascii_art.EXTENDED,
|
|
441
|
+
ColorSupport.ANSI_4BIT: ansi.REV_RAINBOW_16,
|
|
442
|
+
ColorSupport.ANSI_8BIT: ansi.REV_RAINBOW,
|
|
443
|
+
ColorSupport.ANSI_24BIT: truecolor.REV_RAINBOW,
|
|
444
|
+
}
|
|
445
|
+
)
|
|
446
|
+
|
|
418
447
|
|
|
419
448
|
def pick_colormap(maps: dict[ColorSupport, Callable]) -> Callable:
|
|
420
449
|
"""Detect color support and pick the best color map"""
|
|
@@ -422,21 +451,33 @@ def pick_colormap(maps: dict[ColorSupport, Callable]) -> Callable:
|
|
|
422
451
|
return maps[support]
|
|
423
452
|
|
|
424
453
|
|
|
425
|
-
def plot(data, colors=FADE_IN, **plotargs):
|
|
426
|
-
"""Wrapper for plot.Plot that picks colormap from dict
|
|
454
|
+
def plot(data, colors=FADE_IN, colorscale=False, **plotargs):
|
|
455
|
+
"""Wrapper for plot.Plot that picks colormap from dict
|
|
456
|
+
|
|
457
|
+
Parameters
|
|
458
|
+
----------
|
|
459
|
+
data : Sequence[Sequence[float]]
|
|
460
|
+
The data to be plotted.
|
|
461
|
+
colors : Dict mapping color support to color map
|
|
462
|
+
The color scale (e.g. GRAYSCALE, FADE_IN)
|
|
463
|
+
colorscale: bool
|
|
464
|
+
Display a vertical color scale on the right
|
|
465
|
+
(for horizontal, just directly create a colorbar
|
|
466
|
+
and output it after outputting the plot)
|
|
467
|
+
plotargs: any Plot() keyword arguments
|
|
468
|
+
"""
|
|
427
469
|
colormap = pick_colormap(colors)
|
|
428
|
-
|
|
470
|
+
the_plot = plotting.Plot(data, colormap, **plotargs)
|
|
471
|
+
if colorscale:
|
|
472
|
+
colorbar.add_colorbar(the_plot)
|
|
473
|
+
return the_plot
|
|
429
474
|
|
|
430
475
|
|
|
431
476
|
def histplot2d(
|
|
432
477
|
points: Sequence[tuple[FloatLike, FloatLike]],
|
|
433
|
-
bins:
|
|
434
|
-
int
|
|
435
|
-
| tuple[int, int]
|
|
436
|
-
| Sequence[FloatLike]
|
|
437
|
-
| tuple[Sequence[FloatLike], Sequence[FloatLike]]
|
|
438
|
-
) = 10,
|
|
478
|
+
bins: binning.FullBinsArg = None,
|
|
439
479
|
ranges: Optional[tuple[Optional[ValueRange], Optional[ValueRange]]] = None,
|
|
480
|
+
bin_size: Optional[FloatLike | tuple[FloatLike, FloatLike]] = None,
|
|
440
481
|
align=True,
|
|
441
482
|
drop_outside=True,
|
|
442
483
|
colors=FADE_IN,
|
|
@@ -449,8 +490,9 @@ def histplot2d(
|
|
|
449
490
|
"""Wrapper for binning.histogram2d / plot.Plot to simplify 2-D histogram plotting"""
|
|
450
491
|
binned_data, x_axis, y_axis = binning.histogram2d(
|
|
451
492
|
points,
|
|
452
|
-
bins,
|
|
493
|
+
bins=bins,
|
|
453
494
|
ranges=ranges,
|
|
495
|
+
bin_size=bin_size,
|
|
454
496
|
align=align,
|
|
455
497
|
drop_outside=drop_outside,
|
|
456
498
|
border_line=border_line,
|
|
@@ -460,6 +502,82 @@ def histplot2d(
|
|
|
460
502
|
if scale is True:
|
|
461
503
|
p.upscale()
|
|
462
504
|
elif scale:
|
|
463
|
-
p.upscale(max_expansion=
|
|
505
|
+
p.upscale(max_expansion=scale)
|
|
506
|
+
|
|
507
|
+
return p
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def densityplot2d(
|
|
511
|
+
points: Sequence[tuple[FloatLike, FloatLike]],
|
|
512
|
+
kernel: Optional[smoothing.SmoothingFunc] = None,
|
|
513
|
+
bins: binning.FullBinsArg = None,
|
|
514
|
+
ranges: Optional[tuple[Optional[ValueRange], Optional[ValueRange]]] = None,
|
|
515
|
+
align=True,
|
|
516
|
+
colors=FADE_IN,
|
|
517
|
+
border_line=True,
|
|
518
|
+
fractional_tick_pos=False,
|
|
519
|
+
**plotargs,
|
|
520
|
+
# pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals
|
|
521
|
+
):
|
|
522
|
+
"""Wrapper for smoothing.smooth2d / plot.Plot to simplify 2-D density plots"""
|
|
523
|
+
|
|
524
|
+
if not bins:
|
|
525
|
+
try:
|
|
526
|
+
terminal_size: Optional[os.terminal_size] = os.get_terminal_size()
|
|
527
|
+
except OSError:
|
|
528
|
+
terminal_size = plotting.default_terminal_size
|
|
529
|
+
if terminal_size is None:
|
|
530
|
+
raise OSError("No terminal size from os.get_terminal_size()")
|
|
531
|
+
size_x = terminal_size.columns - 10
|
|
532
|
+
size_y = terminal_size.lines - 4
|
|
533
|
+
bins = (size_x, size_y)
|
|
534
|
+
|
|
535
|
+
if kernel is None:
|
|
536
|
+
_, num_bins, bin_centers = binning.expand_bins_arg(bins)
|
|
537
|
+
if bin_centers:
|
|
538
|
+
# we were given a list of bin centers, so generate bounds from that:
|
|
539
|
+
x_bin_range = make_value_range((bin_centers[0][0], bin_centers[0][-1]))
|
|
540
|
+
y_bin_range = make_value_range((bin_centers[1][0], bin_centers[1][-1]))
|
|
541
|
+
x_width, y_width = smoothing.pick_kernel_bandwidth(
|
|
542
|
+
points, bins=num_bins, ranges=(x_bin_range, y_bin_range)
|
|
543
|
+
)
|
|
544
|
+
else:
|
|
545
|
+
x_width, y_width = smoothing.pick_kernel_bandwidth(points, bins=num_bins)
|
|
546
|
+
kernel = smoothing.gaussian_with_sigmas(x_width, y_width)
|
|
547
|
+
|
|
548
|
+
smoothed, x_axis, y_axis = smoothing.smooth2d(
|
|
549
|
+
points=points,
|
|
550
|
+
kernel=kernel,
|
|
551
|
+
bins=bins,
|
|
552
|
+
ranges=ranges,
|
|
553
|
+
align=align,
|
|
554
|
+
border_line=border_line,
|
|
555
|
+
fractional_tick_pos=fractional_tick_pos,
|
|
556
|
+
)
|
|
557
|
+
p = plot(smoothed, colors, x_axis=x_axis, y_axis=y_axis, **plotargs)
|
|
464
558
|
|
|
465
559
|
return p
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def grid_heatmap(data, x_labels, y_labels, colors=REV_RAINBOW, max_cell_size=0, **plotargs):
|
|
563
|
+
"""Create a grid-style heatmap, with explicit X and Y labels for each bin"""
|
|
564
|
+
if max_cell_size == 0:
|
|
565
|
+
max_x_label = max(len(x) for x in x_labels)
|
|
566
|
+
max_cell_size = max_x_label + 2
|
|
567
|
+
|
|
568
|
+
num_rows = len(data)
|
|
569
|
+
num_cols = len(data[0])
|
|
570
|
+
if y_labels and len(y_labels) != num_rows:
|
|
571
|
+
raise ValueError("Number of Y labels does not match number of rows")
|
|
572
|
+
if x_labels and len(x_labels) != num_cols:
|
|
573
|
+
raise ValueError("Number of X labels does not match number of columns")
|
|
574
|
+
|
|
575
|
+
# value range along axis is arbitrary, but is needed to locate the labels along the axis
|
|
576
|
+
# so just use integers 0..N-1
|
|
577
|
+
x_axis = axis.Axis((0, num_cols - 1), dict(enumerate(x_labels)))
|
|
578
|
+
y_axis = axis.Axis((0, num_rows - 1), dict(enumerate(y_labels)))
|
|
579
|
+
|
|
580
|
+
plt = plot(data, colors=colors, x_axis=x_axis, y_axis=y_axis, flip_y=False, **plotargs)
|
|
581
|
+
plt.upscale(max_expansion=max_cell_size)
|
|
582
|
+
|
|
583
|
+
return plt
|
densitty/{plot.py → plotting.py}
RENAMED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
"""Two-dimensional histogram (density plot) with textual output."""
|
|
2
2
|
|
|
3
3
|
import dataclasses
|
|
4
|
-
from itertools import chain, zip_longest
|
|
5
4
|
import os
|
|
6
5
|
import sys
|
|
7
|
-
|
|
6
|
+
import typing
|
|
7
|
+
from itertools import chain, zip_longest
|
|
8
|
+
from typing import Any, Callable, Optional, Sequence
|
|
9
|
+
|
|
10
|
+
from . import ansi, lineart
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
from .
|
|
12
|
+
if typing.TYPE_CHECKING:
|
|
13
|
+
from .axis import Axis
|
|
14
|
+
from .util import FloatLike
|
|
15
|
+
else:
|
|
16
|
+
Axis = Any
|
|
17
|
+
FloatLike = Any
|
|
11
18
|
|
|
12
19
|
# pylint: disable=invalid-name
|
|
13
20
|
# User can set this to provide a default if os.terminal_size() fails:
|
|
@@ -27,9 +34,17 @@ class Plot:
|
|
|
27
34
|
font_mapping: dict = dataclasses.field(default_factory=lambda: lineart.basic_font)
|
|
28
35
|
min_data: Optional[FloatLike] = None
|
|
29
36
|
max_data: Optional[FloatLike] = None
|
|
30
|
-
x_axis: Optional[
|
|
31
|
-
y_axis: Optional[
|
|
37
|
+
x_axis: Optional[Axis] = None
|
|
38
|
+
y_axis: Optional[Axis] = None
|
|
32
39
|
flip_y: bool = True # put the first row of data at the bottom of the output
|
|
40
|
+
to_right: Optional["Plot" | Sequence[str]] = None
|
|
41
|
+
right_padding: Optional[str] = None
|
|
42
|
+
|
|
43
|
+
def data_limits(self):
|
|
44
|
+
"""Return (min,max) of the plot data."""
|
|
45
|
+
min_data = min(min(line) for line in self.data) if self.min_data is None else self.min_data
|
|
46
|
+
max_data = max(max(line) for line in self.data) if self.max_data is None else self.max_data
|
|
47
|
+
return (min_data, max_data)
|
|
33
48
|
|
|
34
49
|
def as_ascii(self):
|
|
35
50
|
"""Output using direct characters (ASCII-art)."""
|
|
@@ -66,8 +81,7 @@ class Plot:
|
|
|
66
81
|
Also flips data if requested
|
|
67
82
|
"""
|
|
68
83
|
|
|
69
|
-
min_data =
|
|
70
|
-
max_data = max(max(line) for line in self.data) if self.max_data is None else self.max_data
|
|
84
|
+
min_data, max_data = self.data_limits()
|
|
71
85
|
data_scale = max_data - min_data
|
|
72
86
|
if data_scale == 0:
|
|
73
87
|
# all data has the same value (or we were told it does)
|
|
@@ -84,6 +98,17 @@ class Plot:
|
|
|
84
98
|
"""Are there two pixels per output character?"""
|
|
85
99
|
return self.is_color() and self.render_halfheight
|
|
86
100
|
|
|
101
|
+
def left_margin(self):
|
|
102
|
+
"""Returns the column of the first plotted data"""
|
|
103
|
+
if self.y_axis:
|
|
104
|
+
if self.is_halfheight():
|
|
105
|
+
num_rows = (len(self.data) + 1) // 2
|
|
106
|
+
else:
|
|
107
|
+
num_rows = len(self.data)
|
|
108
|
+
y_axis_lines = self.y_axis.render_as_y(num_rows, False, False, False)
|
|
109
|
+
return lineart.display_len(y_axis_lines[0])
|
|
110
|
+
return 0
|
|
111
|
+
|
|
87
112
|
def as_strings(self):
|
|
88
113
|
"""Scale data to 0..1 range and feed it through the appropriate output function"""
|
|
89
114
|
if self.is_halfheight():
|
|
@@ -98,22 +123,34 @@ class Plot:
|
|
|
98
123
|
|
|
99
124
|
if self.y_axis:
|
|
100
125
|
axis_lines = self.y_axis.render_as_y(num_rows, False, bool(self.x_axis), self.flip_y)
|
|
126
|
+
left_margin = lineart.display_len(axis_lines[0])
|
|
101
127
|
else:
|
|
102
128
|
axis_lines = ["" for _ in range(num_rows + bool(self.x_axis))]
|
|
129
|
+
left_margin = 0
|
|
103
130
|
|
|
104
|
-
left_margin = lineart.display_len(axis_lines[0])
|
|
105
131
|
if self.x_axis:
|
|
106
132
|
x_ticks, x_labels = self.x_axis.render_as_x(num_cols, left_margin)
|
|
107
133
|
axis_lines[-1] = lineart.merge_lines(x_ticks, axis_lines[-1])
|
|
108
134
|
axis_lines += [x_labels]
|
|
109
135
|
|
|
110
|
-
|
|
111
|
-
|
|
136
|
+
if isinstance(self.to_right, Plot):
|
|
137
|
+
lines_to_right = self.to_right.as_strings()
|
|
138
|
+
elif self.to_right is not None:
|
|
139
|
+
lines_to_right = self.to_right
|
|
140
|
+
else:
|
|
141
|
+
lines_to_right = []
|
|
142
|
+
|
|
143
|
+
padding = self.right_padding if self.right_padding is not None else ""
|
|
144
|
+
|
|
145
|
+
for frame, plot_line, glued_on in zip_longest(
|
|
146
|
+
axis_lines, plot_lines, lines_to_right, fillvalue=""
|
|
147
|
+
):
|
|
148
|
+
yield frame.translate(self.font_mapping) + plot_line + padding + glued_on
|
|
112
149
|
|
|
113
|
-
def show(self, printer=print):
|
|
150
|
+
def show(self, prefix="", printer=print):
|
|
114
151
|
"""Simple helper function to output/print a plot"""
|
|
115
152
|
for line in self.as_strings():
|
|
116
|
-
printer(line)
|
|
153
|
+
printer(prefix + line)
|
|
117
154
|
|
|
118
155
|
def _compute_scaling_multipliers(
|
|
119
156
|
self,
|
|
@@ -156,24 +193,29 @@ class Plot:
|
|
|
156
193
|
|
|
157
194
|
def upscale(
|
|
158
195
|
self,
|
|
159
|
-
max_size: tuple[int, int] =
|
|
160
|
-
max_expansion: tuple[int, int] =
|
|
161
|
-
keep_aspect_ratio: bool =
|
|
196
|
+
max_size: int | tuple[int, int] = 0,
|
|
197
|
+
max_expansion: int | tuple[int, int] = 3,
|
|
198
|
+
keep_aspect_ratio: bool = True,
|
|
162
199
|
):
|
|
163
200
|
"""Scale up 'data' by repeating lines and values within lines.
|
|
164
201
|
|
|
165
202
|
Parameters
|
|
166
203
|
----------
|
|
167
|
-
max_size : tuple (int, int)
|
|
204
|
+
max_size : int or tuple (int, int)
|
|
168
205
|
If positive: Maximum number of columns, maximum number of rows
|
|
169
206
|
If zero: Use terminal size
|
|
170
207
|
If negative: Use as offset from terminal size
|
|
171
208
|
Default: Based on terminal size (0).
|
|
172
|
-
max_expansion : tuple (int, int)
|
|
173
|
-
maximum expansion factor in each direction. Default (3,3).
|
|
209
|
+
max_expansion : int or tuple (int, int)
|
|
210
|
+
maximum expansion factor in each direction. Default (3,3). None=> No maximum
|
|
174
211
|
keep_aspect_ratio : bool
|
|
175
212
|
Require that X and Y scaling are equal.
|
|
176
213
|
"""
|
|
214
|
+
if isinstance(max_size, int):
|
|
215
|
+
max_size = (max_size, max_size)
|
|
216
|
+
|
|
217
|
+
if isinstance(max_expansion, int) or max_expansion is None:
|
|
218
|
+
max_expansion = (max_expansion, max_expansion)
|
|
177
219
|
|
|
178
220
|
float_mult = self._compute_scaling_multipliers(max_size, keep_aspect_ratio)
|
|
179
221
|
col_mult = max(int(float_mult[0]), 1)
|
|
@@ -198,4 +240,20 @@ class Plot:
|
|
|
198
240
|
|
|
199
241
|
# repeat each of those by the row multiplier
|
|
200
242
|
self.data = repeat_each(x_expanded, row_mult)
|
|
243
|
+
|
|
244
|
+
# If we have axes, adjust for the new pixel/bin size
|
|
245
|
+
if self.x_axis:
|
|
246
|
+
self.x_axis.upscale(len(self.data[0]), col_mult)
|
|
247
|
+
|
|
248
|
+
if self.y_axis:
|
|
249
|
+
self.y_axis.upscale(len(self.data), row_mult)
|
|
250
|
+
|
|
251
|
+
# if we have a glued-on plot to the right (likely a colorbar), scale it up in Y to match
|
|
252
|
+
if isinstance(self.to_right, Plot):
|
|
253
|
+
self.to_right.upscale(max_size=0, max_expansion=(1, col_mult), keep_aspect_ratio=False)
|
|
201
254
|
return self
|
|
255
|
+
|
|
256
|
+
def glue_on(self, to_right, padding=" "):
|
|
257
|
+
"""Add lines or another plot to the right of this one."""
|
|
258
|
+
self.right_padding = padding
|
|
259
|
+
self.to_right = to_right
|