plotilleresample 0.4__tar.gz → 1.0__tar.gz
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.
- plotilleresample-1.0/LICENSE +21 -0
- plotilleresample-1.0/PKG-INFO +114 -0
- plotilleresample-1.0/README.md +92 -0
- plotilleresample-1.0/plotilleresample/__init__.py +15 -0
- plotilleresample-1.0/plotilleresample/plotilleresample.py +284 -0
- plotilleresample-1.0/plotilleresample.egg-info/PKG-INFO +114 -0
- {plotilleresample-0.4 → plotilleresample-1.0}/plotilleresample.egg-info/SOURCES.txt +4 -2
- plotilleresample-1.0/pyproject.toml +50 -0
- plotilleresample-1.0/tests/test_plotilleresample.py +240 -0
- plotilleresample-0.4/PKG-INFO +0 -81
- plotilleresample-0.4/README.md +0 -60
- plotilleresample-0.4/plotilleresample/__init__.py +0 -4
- plotilleresample-0.4/plotilleresample/plotilleresample.py +0 -75
- plotilleresample-0.4/plotilleresample.egg-info/PKG-INFO +0 -81
- plotilleresample-0.4/setup.py +0 -34
- {plotilleresample-0.4 → plotilleresample-1.0}/plotilleresample.egg-info/dependency_links.txt +0 -0
- {plotilleresample-0.4 → plotilleresample-1.0}/plotilleresample.egg-info/top_level.txt +0 -0
- {plotilleresample-0.4 → plotilleresample-1.0}/setup.cfg +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019 Carlos A. Planchón
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: plotilleresample
|
|
3
|
+
Version: 1.0
|
|
4
|
+
Summary: Python module to resample datasets before plotting with Plotille.
|
|
5
|
+
Author-email: "Carlos A. Planchón" <carlosandresplanchonprestes@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/carlosplanchon/plotilleresample
|
|
8
|
+
Keywords: plotting,ascii,math,resample
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
11
|
+
Classifier: Topic :: Terminals
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# plotilleresample
|
|
24
|
+

|
|
25
|
+
|
|
26
|
+
*Python module to resample datasets before plotting with Plotille.*
|
|
27
|
+
|
|
28
|
+
[](https://github.com/carlosplanchon/plotilleresample/actions/workflows/ci.yml)
|
|
29
|
+
[](https://pypi.org/project/plotilleresample/)
|
|
30
|
+
[](https://pypi.org/project/plotilleresample/)
|
|
31
|
+
[](https://opensource.org/licenses/MIT)
|
|
32
|
+
|
|
33
|
+
## Why resample?
|
|
34
|
+
|
|
35
|
+
Plotille rasterizes to a terminal canvas of braille dots: `width * 2` columns by `height * 4` rows. Feeding it far more points than that costs interpolation time on detail the canvas cannot show. Plotille rasterizes; plotilleresample decides what information deserves to reach the rasterizer:
|
|
36
|
+
|
|
37
|
+
| Function | Strategy | Best for |
|
|
38
|
+
| ---------------------- | ------------------ | --------------------------------- |
|
|
39
|
+
| `resample_plot` | uniform stride | smooth lines, cheapest reduction |
|
|
40
|
+
| `resample_plot_minmax` | min/max per bucket | peaks, oscillations, time series |
|
|
41
|
+
| `resample_plot_lttb` | largest triangle per bucket | shape-faithful single line |
|
|
42
|
+
| `resample_plot_minmax_lttb` | minmax preselection + LTTB | shape-faithful line, large inputs |
|
|
43
|
+
| `resample_scatter` | uniform stride | large scatter inputs |
|
|
44
|
+
|
|
45
|
+
The uniform stride keeps one point every N: fast and predictable, but a narrow peak that falls between two kept points disappears from the plot. `resample_plot_minmax` instead makes one bucket per braille dot column and keeps the minimum and the maximum Y of each bucket, so the envelope of the signal — spikes included — always survives:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
X = list(range(10000))
|
|
49
|
+
Y = [0.0] * 10000
|
|
50
|
+
Y[33] = 1000.0 # a narrow spike
|
|
51
|
+
|
|
52
|
+
_, y_stride = plotilleresample.resample_plot(X, Y)
|
|
53
|
+
_, y_minmax = plotilleresample.resample_plot_minmax(X, Y)
|
|
54
|
+
|
|
55
|
+
1000.0 in y_stride # False: the spike vanished
|
|
56
|
+
1000.0 in y_minmax # True: the envelope survives
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`resample_plot_lttb` implements Largest-Triangle-Three-Buckets (Steinarsson, 2013): it always keeps the first and the last point and picks the most shape-representative point of each bucket, giving a single clean line that looks like the original. It keeps one point per bucket, so — unlike min/max — one of two opposing extremes falling in the same bucket can be dropped: shape fidelity instead of envelope guarantee.
|
|
60
|
+
|
|
61
|
+
`resample_plot_minmax_lttb` is the hybrid (MinMaxLTTB, Van der Donckt et al., 2023; the plotly-resampler default): on large inputs a minmax pass preselects the per bucket extremes and LTTB runs over those candidates only. Visually close to pure LTTB and faster, with a gap that grows with input size — about 1.6x end to end at 100,000 points, and about 3.4x on the resampling pass alone at 1,000,000 — and the true extremes are always among the candidates.
|
|
62
|
+
|
|
63
|
+
All four plot resamplers work in sample order — they bucket by index, so X is expected to be already sorted, as in a time series (`resample_scatter` makes no ordering assumption). They keep at most `width * 4` points and `resample_scatter` keeps at most `width * 2 * height`, so plotille only receives what the canvas can actually display.
|
|
64
|
+
|
|
65
|
+
## Benchmark
|
|
66
|
+
|
|
67
|
+
End to end times: building the plot string with plotille alone versus resampling first. Measured with `benchmarks/bench.py` (canvas 80x40, best of 3) on Python 3.14, Linux, Intel Core i5-1135G7:
|
|
68
|
+
|
|
69
|
+
| Points | plotille alone | stride + plotille | minmax + plotille | lttb + plotille | mmlttb + plotille |
|
|
70
|
+
| ------- | -------------- | ----------------- | ----------------- | --------------- | ----------------- |
|
|
71
|
+
| 10,000 | 202 ms | 23 ms | 22 ms | 27 ms | 24 ms |
|
|
72
|
+
| 100,000 | 1.75 s | 39 ms | 52 ms | 90 ms | 56 ms |
|
|
73
|
+
|
|
74
|
+
Reproduce it from the repository root with:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
uv run --group bench benchmarks/bench.py
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The resampling-pass figure quoted in Why resample? (pure LTTB vs MinMaxLTTB at 1,000,000 points) has its own script:
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
uv run benchmarks/bench_resamplers.py
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Installation
|
|
87
|
+
### Install with UV:
|
|
88
|
+
```
|
|
89
|
+
uv add plotilleresample
|
|
90
|
+
```
|
|
91
|
+
### Install with pip:
|
|
92
|
+
```
|
|
93
|
+
pip install plotilleresample
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
plotilleresample has no runtime dependencies — not even plotille: it only reduces sequences. To run the example below, install plotille as well (`uv add plotille` or `pip install plotille`).
|
|
97
|
+
|
|
98
|
+
## Usage
|
|
99
|
+
```python
|
|
100
|
+
import math
|
|
101
|
+
|
|
102
|
+
import plotille
|
|
103
|
+
|
|
104
|
+
from plotilleresample import resample_plot_minmax_lttb
|
|
105
|
+
|
|
106
|
+
r = 100_000
|
|
107
|
+
X = list(range(r))
|
|
108
|
+
Y = [math.sin(i / 500) * 100 for i in range(r)]
|
|
109
|
+
|
|
110
|
+
X, Y = resample_plot_minmax_lttb(X, Y, width=80, height=40)
|
|
111
|
+
print(plotille.plot(X, Y, width=80, height=40))
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The full interactive demo, running every resampler on the same dataset, lives in [`examples/demo.py`](examples/demo.py).
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# plotilleresample
|
|
2
|
+

|
|
3
|
+
|
|
4
|
+
*Python module to resample datasets before plotting with Plotille.*
|
|
5
|
+
|
|
6
|
+
[](https://github.com/carlosplanchon/plotilleresample/actions/workflows/ci.yml)
|
|
7
|
+
[](https://pypi.org/project/plotilleresample/)
|
|
8
|
+
[](https://pypi.org/project/plotilleresample/)
|
|
9
|
+
[](https://opensource.org/licenses/MIT)
|
|
10
|
+
|
|
11
|
+
## Why resample?
|
|
12
|
+
|
|
13
|
+
Plotille rasterizes to a terminal canvas of braille dots: `width * 2` columns by `height * 4` rows. Feeding it far more points than that costs interpolation time on detail the canvas cannot show. Plotille rasterizes; plotilleresample decides what information deserves to reach the rasterizer:
|
|
14
|
+
|
|
15
|
+
| Function | Strategy | Best for |
|
|
16
|
+
| ---------------------- | ------------------ | --------------------------------- |
|
|
17
|
+
| `resample_plot` | uniform stride | smooth lines, cheapest reduction |
|
|
18
|
+
| `resample_plot_minmax` | min/max per bucket | peaks, oscillations, time series |
|
|
19
|
+
| `resample_plot_lttb` | largest triangle per bucket | shape-faithful single line |
|
|
20
|
+
| `resample_plot_minmax_lttb` | minmax preselection + LTTB | shape-faithful line, large inputs |
|
|
21
|
+
| `resample_scatter` | uniform stride | large scatter inputs |
|
|
22
|
+
|
|
23
|
+
The uniform stride keeps one point every N: fast and predictable, but a narrow peak that falls between two kept points disappears from the plot. `resample_plot_minmax` instead makes one bucket per braille dot column and keeps the minimum and the maximum Y of each bucket, so the envelope of the signal — spikes included — always survives:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
X = list(range(10000))
|
|
27
|
+
Y = [0.0] * 10000
|
|
28
|
+
Y[33] = 1000.0 # a narrow spike
|
|
29
|
+
|
|
30
|
+
_, y_stride = plotilleresample.resample_plot(X, Y)
|
|
31
|
+
_, y_minmax = plotilleresample.resample_plot_minmax(X, Y)
|
|
32
|
+
|
|
33
|
+
1000.0 in y_stride # False: the spike vanished
|
|
34
|
+
1000.0 in y_minmax # True: the envelope survives
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`resample_plot_lttb` implements Largest-Triangle-Three-Buckets (Steinarsson, 2013): it always keeps the first and the last point and picks the most shape-representative point of each bucket, giving a single clean line that looks like the original. It keeps one point per bucket, so — unlike min/max — one of two opposing extremes falling in the same bucket can be dropped: shape fidelity instead of envelope guarantee.
|
|
38
|
+
|
|
39
|
+
`resample_plot_minmax_lttb` is the hybrid (MinMaxLTTB, Van der Donckt et al., 2023; the plotly-resampler default): on large inputs a minmax pass preselects the per bucket extremes and LTTB runs over those candidates only. Visually close to pure LTTB and faster, with a gap that grows with input size — about 1.6x end to end at 100,000 points, and about 3.4x on the resampling pass alone at 1,000,000 — and the true extremes are always among the candidates.
|
|
40
|
+
|
|
41
|
+
All four plot resamplers work in sample order — they bucket by index, so X is expected to be already sorted, as in a time series (`resample_scatter` makes no ordering assumption). They keep at most `width * 4` points and `resample_scatter` keeps at most `width * 2 * height`, so plotille only receives what the canvas can actually display.
|
|
42
|
+
|
|
43
|
+
## Benchmark
|
|
44
|
+
|
|
45
|
+
End to end times: building the plot string with plotille alone versus resampling first. Measured with `benchmarks/bench.py` (canvas 80x40, best of 3) on Python 3.14, Linux, Intel Core i5-1135G7:
|
|
46
|
+
|
|
47
|
+
| Points | plotille alone | stride + plotille | minmax + plotille | lttb + plotille | mmlttb + plotille |
|
|
48
|
+
| ------- | -------------- | ----------------- | ----------------- | --------------- | ----------------- |
|
|
49
|
+
| 10,000 | 202 ms | 23 ms | 22 ms | 27 ms | 24 ms |
|
|
50
|
+
| 100,000 | 1.75 s | 39 ms | 52 ms | 90 ms | 56 ms |
|
|
51
|
+
|
|
52
|
+
Reproduce it from the repository root with:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
uv run --group bench benchmarks/bench.py
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The resampling-pass figure quoted in Why resample? (pure LTTB vs MinMaxLTTB at 1,000,000 points) has its own script:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
uv run benchmarks/bench_resamplers.py
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Installation
|
|
65
|
+
### Install with UV:
|
|
66
|
+
```
|
|
67
|
+
uv add plotilleresample
|
|
68
|
+
```
|
|
69
|
+
### Install with pip:
|
|
70
|
+
```
|
|
71
|
+
pip install plotilleresample
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
plotilleresample has no runtime dependencies — not even plotille: it only reduces sequences. To run the example below, install plotille as well (`uv add plotille` or `pip install plotille`).
|
|
75
|
+
|
|
76
|
+
## Usage
|
|
77
|
+
```python
|
|
78
|
+
import math
|
|
79
|
+
|
|
80
|
+
import plotille
|
|
81
|
+
|
|
82
|
+
from plotilleresample import resample_plot_minmax_lttb
|
|
83
|
+
|
|
84
|
+
r = 100_000
|
|
85
|
+
X = list(range(r))
|
|
86
|
+
Y = [math.sin(i / 500) * 100 for i in range(r)]
|
|
87
|
+
|
|
88
|
+
X, Y = resample_plot_minmax_lttb(X, Y, width=80, height=40)
|
|
89
|
+
print(plotille.plot(X, Y, width=80, height=40))
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The full interactive demo, running every resampler on the same dataset, lives in [`examples/demo.py`](examples/demo.py).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
from plotilleresample.plotilleresample import resample_plot
|
|
4
|
+
from plotilleresample.plotilleresample import resample_plot_lttb
|
|
5
|
+
from plotilleresample.plotilleresample import resample_plot_minmax
|
|
6
|
+
from plotilleresample.plotilleresample import resample_plot_minmax_lttb
|
|
7
|
+
from plotilleresample.plotilleresample import resample_scatter
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"resample_plot",
|
|
11
|
+
"resample_plot_lttb",
|
|
12
|
+
"resample_plot_minmax",
|
|
13
|
+
"resample_plot_minmax_lttb",
|
|
14
|
+
"resample_scatter",
|
|
15
|
+
]
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from math import ceil
|
|
5
|
+
|
|
6
|
+
# Historically chosen to reduce Nyquist/undersampling-related artifacts.
|
|
7
|
+
plot_multiplier = 4
|
|
8
|
+
|
|
9
|
+
# Preselection ratio for resample_plot_minmax_lttb, as in plotly-resampler.
|
|
10
|
+
minmax_ratio = 4
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _check_input(
|
|
14
|
+
X: Sequence[float],
|
|
15
|
+
Y: Sequence[float],
|
|
16
|
+
width: int
|
|
17
|
+
) -> None:
|
|
18
|
+
if len(X) != len(Y):
|
|
19
|
+
raise ValueError(
|
|
20
|
+
"X and Y must have the same number of entries: "
|
|
21
|
+
f"{len(X)} != {len(Y)}"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
if width <= 0:
|
|
25
|
+
raise ValueError(f"width must be positive: {width}")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _minmax_indices(
|
|
29
|
+
Y: Sequence[float],
|
|
30
|
+
buckets: int
|
|
31
|
+
) -> list[int]:
|
|
32
|
+
n = len(Y)
|
|
33
|
+
|
|
34
|
+
idxs = []
|
|
35
|
+
for b in range(buckets):
|
|
36
|
+
start = b * n // buckets
|
|
37
|
+
stop = (b + 1) * n // buckets
|
|
38
|
+
|
|
39
|
+
i_min = i_max = start
|
|
40
|
+
for i in range(start + 1, stop):
|
|
41
|
+
if Y[i] < Y[i_min]:
|
|
42
|
+
i_min = i
|
|
43
|
+
elif Y[i] > Y[i_max]:
|
|
44
|
+
i_max = i
|
|
45
|
+
|
|
46
|
+
# In index order so a sorted X stays sorted.
|
|
47
|
+
idxs.extend(sorted({i_min, i_max}))
|
|
48
|
+
|
|
49
|
+
return idxs
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _lttb(
|
|
53
|
+
X: Sequence[float],
|
|
54
|
+
Y: Sequence[float],
|
|
55
|
+
n_out: int
|
|
56
|
+
) -> tuple[list[float], list[float]]:
|
|
57
|
+
n = len(X)
|
|
58
|
+
|
|
59
|
+
# First and last point always survive; one point per bucket
|
|
60
|
+
# in between, for exactly n_out points.
|
|
61
|
+
m = n_out - 2
|
|
62
|
+
|
|
63
|
+
new_X = [X[0]]
|
|
64
|
+
new_Y = [Y[0]]
|
|
65
|
+
|
|
66
|
+
a = 0
|
|
67
|
+
for b in range(m):
|
|
68
|
+
start = 1 + b * (n - 2) // m
|
|
69
|
+
stop = 1 + (b + 1) * (n - 2) // m
|
|
70
|
+
|
|
71
|
+
# A is the previously selected point; C is the average of
|
|
72
|
+
# the next bucket (the last point for the final bucket).
|
|
73
|
+
if b + 1 < m:
|
|
74
|
+
next_stop = 1 + (b + 2) * (n - 2) // m
|
|
75
|
+
span = next_stop - stop
|
|
76
|
+
c_x = sum(X[stop:next_stop]) / span
|
|
77
|
+
c_y = sum(Y[stop:next_stop]) / span
|
|
78
|
+
else:
|
|
79
|
+
c_x = X[n - 1]
|
|
80
|
+
c_y = Y[n - 1]
|
|
81
|
+
|
|
82
|
+
a_x = X[a]
|
|
83
|
+
a_y = Y[a]
|
|
84
|
+
|
|
85
|
+
best = start
|
|
86
|
+
best_area = -1.0
|
|
87
|
+
for i in range(start, stop):
|
|
88
|
+
area = abs(
|
|
89
|
+
(a_x - c_x) * (Y[i] - a_y)
|
|
90
|
+
- (a_x - X[i]) * (c_y - a_y)
|
|
91
|
+
)
|
|
92
|
+
if area > best_area:
|
|
93
|
+
best_area = area
|
|
94
|
+
best = i
|
|
95
|
+
|
|
96
|
+
new_X.append(X[best])
|
|
97
|
+
new_Y.append(Y[best])
|
|
98
|
+
a = best
|
|
99
|
+
|
|
100
|
+
new_X.append(X[n - 1])
|
|
101
|
+
new_Y.append(Y[n - 1])
|
|
102
|
+
|
|
103
|
+
return new_X, new_Y
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def resample_plot(
|
|
107
|
+
X: Sequence[float],
|
|
108
|
+
Y: Sequence[float],
|
|
109
|
+
width: int = 80,
|
|
110
|
+
height: int = 40
|
|
111
|
+
) -> tuple[Sequence[float], Sequence[float]]:
|
|
112
|
+
"""
|
|
113
|
+
Works in sample order: X is expected to be already sorted,
|
|
114
|
+
as in a time series.
|
|
115
|
+
|
|
116
|
+
:param X: Sequence[float]: X values.
|
|
117
|
+
:param Y: Sequence[float]: Y values.
|
|
118
|
+
:param width: int: Width of the plot. (Default value = 80)
|
|
119
|
+
:param height: int: Unused; kept for signature symmetry
|
|
120
|
+
with resample_scatter. (Default value = 40)
|
|
121
|
+
|
|
122
|
+
"""
|
|
123
|
+
_check_input(X, Y, width)
|
|
124
|
+
|
|
125
|
+
if len(X) > width * plot_multiplier:
|
|
126
|
+
step = ceil(len(X) / (width * plot_multiplier))
|
|
127
|
+
|
|
128
|
+
X = [
|
|
129
|
+
X[i] for i in range(
|
|
130
|
+
0, len(X), step
|
|
131
|
+
)
|
|
132
|
+
]
|
|
133
|
+
Y = [
|
|
134
|
+
Y[i] for i in range(
|
|
135
|
+
0, len(Y), step
|
|
136
|
+
)
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
return X, Y
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def resample_plot_minmax(
|
|
143
|
+
X: Sequence[float],
|
|
144
|
+
Y: Sequence[float],
|
|
145
|
+
width: int = 80,
|
|
146
|
+
height: int = 40
|
|
147
|
+
) -> tuple[Sequence[float], Sequence[float]]:
|
|
148
|
+
"""
|
|
149
|
+
Like resample_plot, but it keeps the minimum and the maximum
|
|
150
|
+
Y of each bucket, so peaks in high frequency data survive
|
|
151
|
+
the resampling. Buckets are formed in sample order: X is
|
|
152
|
+
expected to be already sorted, as in a time series.
|
|
153
|
+
|
|
154
|
+
:param X: Sequence[float]: X values.
|
|
155
|
+
:param Y: Sequence[float]: Y values.
|
|
156
|
+
:param width: int: Width of the plot. (Default value = 80)
|
|
157
|
+
:param height: int: Unused; kept for signature symmetry
|
|
158
|
+
with resample_scatter. (Default value = 40)
|
|
159
|
+
|
|
160
|
+
"""
|
|
161
|
+
_check_input(X, Y, width)
|
|
162
|
+
|
|
163
|
+
if len(X) > width * plot_multiplier:
|
|
164
|
+
# One bucket per braille dot column (2 per char); min and max
|
|
165
|
+
# per bucket keep the output at most width * plot_multiplier.
|
|
166
|
+
idxs = _minmax_indices(Y, width * plot_multiplier // 2)
|
|
167
|
+
|
|
168
|
+
X = [X[i] for i in idxs]
|
|
169
|
+
Y = [Y[i] for i in idxs]
|
|
170
|
+
|
|
171
|
+
return X, Y
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def resample_plot_lttb(
|
|
175
|
+
X: Sequence[float],
|
|
176
|
+
Y: Sequence[float],
|
|
177
|
+
width: int = 80,
|
|
178
|
+
height: int = 40
|
|
179
|
+
) -> tuple[Sequence[float], Sequence[float]]:
|
|
180
|
+
"""
|
|
181
|
+
Like resample_plot, but it applies Largest-Triangle-Three-Buckets
|
|
182
|
+
(Steinarsson, 2013): it always keeps the first and the last point
|
|
183
|
+
and picks the most shape-representative point of each bucket, so
|
|
184
|
+
the reduced line looks like the original. Unlike
|
|
185
|
+
resample_plot_minmax it keeps one point per bucket, so one of two
|
|
186
|
+
opposing extremes falling in the same bucket can be dropped.
|
|
187
|
+
Buckets are formed in sample order: X is expected to be already
|
|
188
|
+
sorted, as in a time series.
|
|
189
|
+
|
|
190
|
+
:param X: Sequence[float]: X values.
|
|
191
|
+
:param Y: Sequence[float]: Y values.
|
|
192
|
+
:param width: int: Width of the plot. (Default value = 80)
|
|
193
|
+
:param height: int: Unused; kept for signature symmetry
|
|
194
|
+
with resample_scatter. (Default value = 40)
|
|
195
|
+
|
|
196
|
+
"""
|
|
197
|
+
_check_input(X, Y, width)
|
|
198
|
+
|
|
199
|
+
if len(X) > width * plot_multiplier:
|
|
200
|
+
X, Y = _lttb(X, Y, width * plot_multiplier)
|
|
201
|
+
|
|
202
|
+
return X, Y
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def resample_plot_minmax_lttb(
|
|
206
|
+
X: Sequence[float],
|
|
207
|
+
Y: Sequence[float],
|
|
208
|
+
width: int = 80,
|
|
209
|
+
height: int = 40
|
|
210
|
+
) -> tuple[Sequence[float], Sequence[float]]:
|
|
211
|
+
"""
|
|
212
|
+
Like resample_plot_lttb, but on large inputs it first preselects
|
|
213
|
+
the per bucket extremes with a minmax pass at minmax_ratio finer
|
|
214
|
+
granularity and runs LTTB over those candidates (MinMaxLTTB,
|
|
215
|
+
Van der Donckt et al., 2023; the plotly-resampler default).
|
|
216
|
+
Faster than pure LTTB, with a gap that grows with input size;
|
|
217
|
+
the true extremes are always among the candidates, and the
|
|
218
|
+
result stays visually close to pure LTTB. Buckets are
|
|
219
|
+
formed in sample order: X is expected to be already sorted,
|
|
220
|
+
as in a time series.
|
|
221
|
+
|
|
222
|
+
:param X: Sequence[float]: X values.
|
|
223
|
+
:param Y: Sequence[float]: Y values.
|
|
224
|
+
:param width: int: Width of the plot. (Default value = 80)
|
|
225
|
+
:param height: int: Unused; kept for signature symmetry
|
|
226
|
+
with resample_scatter. (Default value = 40)
|
|
227
|
+
|
|
228
|
+
"""
|
|
229
|
+
_check_input(X, Y, width)
|
|
230
|
+
|
|
231
|
+
n_out = width * plot_multiplier
|
|
232
|
+
if len(X) > n_out:
|
|
233
|
+
if len(X) > n_out * minmax_ratio:
|
|
234
|
+
idxs = _minmax_indices(Y, n_out * minmax_ratio // 2)
|
|
235
|
+
|
|
236
|
+
# LTTB anchors on the endpoints, so force them in.
|
|
237
|
+
if idxs[0] != 0:
|
|
238
|
+
idxs.insert(0, 0)
|
|
239
|
+
if idxs[-1] != len(X) - 1:
|
|
240
|
+
idxs.append(len(X) - 1)
|
|
241
|
+
|
|
242
|
+
X = [X[i] for i in idxs]
|
|
243
|
+
Y = [Y[i] for i in idxs]
|
|
244
|
+
|
|
245
|
+
X, Y = _lttb(X, Y, n_out)
|
|
246
|
+
|
|
247
|
+
return X, Y
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def resample_scatter(
|
|
251
|
+
X: Sequence[float],
|
|
252
|
+
Y: Sequence[float],
|
|
253
|
+
width: int = 80,
|
|
254
|
+
height: int = 40
|
|
255
|
+
) -> tuple[Sequence[float], Sequence[float]]:
|
|
256
|
+
"""
|
|
257
|
+
|
|
258
|
+
:param X: Sequence[float]: X values.
|
|
259
|
+
:param Y: Sequence[float]: Y values.
|
|
260
|
+
:param width: int: Width of the plot. (Default value = 80)
|
|
261
|
+
:param height: int: Height of the plot. (Default value = 40)
|
|
262
|
+
|
|
263
|
+
"""
|
|
264
|
+
_check_input(X, Y, width)
|
|
265
|
+
|
|
266
|
+
if height <= 0:
|
|
267
|
+
raise ValueError(f"height must be positive: {height}")
|
|
268
|
+
|
|
269
|
+
scatter_multiplier = width * 2 * height
|
|
270
|
+
if len(X) > scatter_multiplier:
|
|
271
|
+
step = ceil(len(X) / scatter_multiplier)
|
|
272
|
+
|
|
273
|
+
X = [
|
|
274
|
+
X[i] for i in range(
|
|
275
|
+
0, len(X), step
|
|
276
|
+
)
|
|
277
|
+
]
|
|
278
|
+
Y = [
|
|
279
|
+
Y[i] for i in range(
|
|
280
|
+
0, len(Y), step
|
|
281
|
+
)
|
|
282
|
+
]
|
|
283
|
+
|
|
284
|
+
return X, Y
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: plotilleresample
|
|
3
|
+
Version: 1.0
|
|
4
|
+
Summary: Python module to resample datasets before plotting with Plotille.
|
|
5
|
+
Author-email: "Carlos A. Planchón" <carlosandresplanchonprestes@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/carlosplanchon/plotilleresample
|
|
8
|
+
Keywords: plotting,ascii,math,resample
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
11
|
+
Classifier: Topic :: Terminals
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# plotilleresample
|
|
24
|
+

|
|
25
|
+
|
|
26
|
+
*Python module to resample datasets before plotting with Plotille.*
|
|
27
|
+
|
|
28
|
+
[](https://github.com/carlosplanchon/plotilleresample/actions/workflows/ci.yml)
|
|
29
|
+
[](https://pypi.org/project/plotilleresample/)
|
|
30
|
+
[](https://pypi.org/project/plotilleresample/)
|
|
31
|
+
[](https://opensource.org/licenses/MIT)
|
|
32
|
+
|
|
33
|
+
## Why resample?
|
|
34
|
+
|
|
35
|
+
Plotille rasterizes to a terminal canvas of braille dots: `width * 2` columns by `height * 4` rows. Feeding it far more points than that costs interpolation time on detail the canvas cannot show. Plotille rasterizes; plotilleresample decides what information deserves to reach the rasterizer:
|
|
36
|
+
|
|
37
|
+
| Function | Strategy | Best for |
|
|
38
|
+
| ---------------------- | ------------------ | --------------------------------- |
|
|
39
|
+
| `resample_plot` | uniform stride | smooth lines, cheapest reduction |
|
|
40
|
+
| `resample_plot_minmax` | min/max per bucket | peaks, oscillations, time series |
|
|
41
|
+
| `resample_plot_lttb` | largest triangle per bucket | shape-faithful single line |
|
|
42
|
+
| `resample_plot_minmax_lttb` | minmax preselection + LTTB | shape-faithful line, large inputs |
|
|
43
|
+
| `resample_scatter` | uniform stride | large scatter inputs |
|
|
44
|
+
|
|
45
|
+
The uniform stride keeps one point every N: fast and predictable, but a narrow peak that falls between two kept points disappears from the plot. `resample_plot_minmax` instead makes one bucket per braille dot column and keeps the minimum and the maximum Y of each bucket, so the envelope of the signal — spikes included — always survives:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
X = list(range(10000))
|
|
49
|
+
Y = [0.0] * 10000
|
|
50
|
+
Y[33] = 1000.0 # a narrow spike
|
|
51
|
+
|
|
52
|
+
_, y_stride = plotilleresample.resample_plot(X, Y)
|
|
53
|
+
_, y_minmax = plotilleresample.resample_plot_minmax(X, Y)
|
|
54
|
+
|
|
55
|
+
1000.0 in y_stride # False: the spike vanished
|
|
56
|
+
1000.0 in y_minmax # True: the envelope survives
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`resample_plot_lttb` implements Largest-Triangle-Three-Buckets (Steinarsson, 2013): it always keeps the first and the last point and picks the most shape-representative point of each bucket, giving a single clean line that looks like the original. It keeps one point per bucket, so — unlike min/max — one of two opposing extremes falling in the same bucket can be dropped: shape fidelity instead of envelope guarantee.
|
|
60
|
+
|
|
61
|
+
`resample_plot_minmax_lttb` is the hybrid (MinMaxLTTB, Van der Donckt et al., 2023; the plotly-resampler default): on large inputs a minmax pass preselects the per bucket extremes and LTTB runs over those candidates only. Visually close to pure LTTB and faster, with a gap that grows with input size — about 1.6x end to end at 100,000 points, and about 3.4x on the resampling pass alone at 1,000,000 — and the true extremes are always among the candidates.
|
|
62
|
+
|
|
63
|
+
All four plot resamplers work in sample order — they bucket by index, so X is expected to be already sorted, as in a time series (`resample_scatter` makes no ordering assumption). They keep at most `width * 4` points and `resample_scatter` keeps at most `width * 2 * height`, so plotille only receives what the canvas can actually display.
|
|
64
|
+
|
|
65
|
+
## Benchmark
|
|
66
|
+
|
|
67
|
+
End to end times: building the plot string with plotille alone versus resampling first. Measured with `benchmarks/bench.py` (canvas 80x40, best of 3) on Python 3.14, Linux, Intel Core i5-1135G7:
|
|
68
|
+
|
|
69
|
+
| Points | plotille alone | stride + plotille | minmax + plotille | lttb + plotille | mmlttb + plotille |
|
|
70
|
+
| ------- | -------------- | ----------------- | ----------------- | --------------- | ----------------- |
|
|
71
|
+
| 10,000 | 202 ms | 23 ms | 22 ms | 27 ms | 24 ms |
|
|
72
|
+
| 100,000 | 1.75 s | 39 ms | 52 ms | 90 ms | 56 ms |
|
|
73
|
+
|
|
74
|
+
Reproduce it from the repository root with:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
uv run --group bench benchmarks/bench.py
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The resampling-pass figure quoted in Why resample? (pure LTTB vs MinMaxLTTB at 1,000,000 points) has its own script:
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
uv run benchmarks/bench_resamplers.py
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Installation
|
|
87
|
+
### Install with UV:
|
|
88
|
+
```
|
|
89
|
+
uv add plotilleresample
|
|
90
|
+
```
|
|
91
|
+
### Install with pip:
|
|
92
|
+
```
|
|
93
|
+
pip install plotilleresample
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
plotilleresample has no runtime dependencies — not even plotille: it only reduces sequences. To run the example below, install plotille as well (`uv add plotille` or `pip install plotille`).
|
|
97
|
+
|
|
98
|
+
## Usage
|
|
99
|
+
```python
|
|
100
|
+
import math
|
|
101
|
+
|
|
102
|
+
import plotille
|
|
103
|
+
|
|
104
|
+
from plotilleresample import resample_plot_minmax_lttb
|
|
105
|
+
|
|
106
|
+
r = 100_000
|
|
107
|
+
X = list(range(r))
|
|
108
|
+
Y = [math.sin(i / 500) * 100 for i in range(r)]
|
|
109
|
+
|
|
110
|
+
X, Y = resample_plot_minmax_lttb(X, Y, width=80, height=40)
|
|
111
|
+
print(plotille.plot(X, Y, width=80, height=40))
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The full interactive demo, running every resampler on the same dataset, lives in [`examples/demo.py`](examples/demo.py).
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
LICENSE
|
|
1
2
|
README.md
|
|
2
|
-
|
|
3
|
+
pyproject.toml
|
|
3
4
|
plotilleresample/__init__.py
|
|
4
5
|
plotilleresample/plotilleresample.py
|
|
5
6
|
plotilleresample.egg-info/PKG-INFO
|
|
6
7
|
plotilleresample.egg-info/SOURCES.txt
|
|
7
8
|
plotilleresample.egg-info/dependency_links.txt
|
|
8
|
-
plotilleresample.egg-info/top_level.txt
|
|
9
|
+
plotilleresample.egg-info/top_level.txt
|
|
10
|
+
tests/test_plotilleresample.py
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0.3", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "plotilleresample"
|
|
7
|
+
version = "1.0"
|
|
8
|
+
description = "Python module to resample datasets before plotting with Plotille."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
|
|
14
|
+
authors = [
|
|
15
|
+
{name = "Carlos A. Planchón", email = "carlosandresplanchonprestes@gmail.com"}
|
|
16
|
+
]
|
|
17
|
+
keywords = ["plotting", "ascii", "math", "resample"]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Topic :: Scientific/Engineering :: Visualization",
|
|
21
|
+
"Topic :: Terminals",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Programming Language :: Python :: 3.14"
|
|
28
|
+
|
|
29
|
+
]
|
|
30
|
+
requires-python = ">=3.10"
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/carlosplanchon/plotilleresample"
|
|
34
|
+
|
|
35
|
+
[tool.setuptools.packages.find]
|
|
36
|
+
where = ["."]
|
|
37
|
+
include = ["plotilleresample*"]
|
|
38
|
+
|
|
39
|
+
[dependency-groups]
|
|
40
|
+
dev = ["pytest>=8"]
|
|
41
|
+
bench = ["plotille"]
|
|
42
|
+
|
|
43
|
+
[tool.pytest.ini_options]
|
|
44
|
+
testpaths = ["tests"]
|
|
45
|
+
|
|
46
|
+
[tool.ruff.lint]
|
|
47
|
+
# Select the classic default ruleset explicitly (pyflakes + a pycodestyle
|
|
48
|
+
# subset) so linting stays deterministic across ruff releases. ruff 0.16
|
|
49
|
+
# widened its implicit default, which broke `uvx ruff check` in CI.
|
|
50
|
+
select = ["E4", "E7", "E9", "F"]
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Tests for plotilleresample.
|
|
5
|
+
|
|
6
|
+
Run with pytest, or directly:
|
|
7
|
+
python tests/test_plotilleresample.py
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import math
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
16
|
+
|
|
17
|
+
from plotilleresample import resample_plot
|
|
18
|
+
from plotilleresample import resample_plot_lttb
|
|
19
|
+
from plotilleresample import resample_plot_minmax
|
|
20
|
+
from plotilleresample import resample_plot_minmax_lttb
|
|
21
|
+
from plotilleresample import resample_scatter
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_resample_plot_size():
|
|
25
|
+
r = 10000
|
|
26
|
+
X = list(range(r))
|
|
27
|
+
Y = [math.sin(i / 100) * 100 for i in range(r)]
|
|
28
|
+
xp, yp = resample_plot(X, Y, 80, 40)
|
|
29
|
+
assert len(xp) == len(yp)
|
|
30
|
+
assert len(xp) <= 80 * 4
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_resample_scatter_size():
|
|
34
|
+
for r in (9000, 10000):
|
|
35
|
+
xs, ys = resample_scatter(list(range(r)), list(range(r)), 80, 40)
|
|
36
|
+
assert len(xs) == len(ys)
|
|
37
|
+
assert len(xs) <= 80 * 2 * 40
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_minmax_passthrough_short_input():
|
|
41
|
+
X = [1.0, 2.0]
|
|
42
|
+
Y = [3.0, 4.0]
|
|
43
|
+
assert resample_plot_minmax(X, Y, 80, 40) == (X, Y)
|
|
44
|
+
assert resample_plot_minmax([], [], 80, 40) == ([], [])
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_minmax_size_within_target():
|
|
48
|
+
r = 10000
|
|
49
|
+
X = list(range(r))
|
|
50
|
+
Y = [math.sin(i / 100) * 100 for i in range(r)]
|
|
51
|
+
xm, ym = resample_plot_minmax(X, Y, 80, 40)
|
|
52
|
+
assert len(xm) == len(ym)
|
|
53
|
+
assert len(xm) <= 80 * 4
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_minmax_x_stays_sorted():
|
|
57
|
+
r = 10000
|
|
58
|
+
X = list(range(r))
|
|
59
|
+
Y = [math.sin(i / 7) * 100 for i in range(r)]
|
|
60
|
+
xm, _ = resample_plot_minmax(X, Y, 80, 40)
|
|
61
|
+
assert all(a <= b for a, b in zip(xm, xm[1:]))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_minmax_preserves_envelope():
|
|
65
|
+
r = 10000
|
|
66
|
+
X = list(range(r))
|
|
67
|
+
Y = [math.sin(i / 100) * 100 for i in range(r)]
|
|
68
|
+
_, ym = resample_plot_minmax(X, Y, 80, 40)
|
|
69
|
+
assert max(ym) == max(Y)
|
|
70
|
+
assert min(ym) == min(Y)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_spike_survives_minmax_but_not_stride():
|
|
74
|
+
# A flat signal with one spike placed off the stride grid:
|
|
75
|
+
# resample_plot with 10000 points and width 80 keeps indices
|
|
76
|
+
# 0, 32, 64, ... (step = ceil(10000 / 320) = 32), so the spike
|
|
77
|
+
# at index 33 is dropped. The minmax bucket containing index 33
|
|
78
|
+
# keeps it as the bucket maximum.
|
|
79
|
+
r = 10000
|
|
80
|
+
X = list(range(r))
|
|
81
|
+
Y = [0.0] * r
|
|
82
|
+
Y[33] = 1000.0
|
|
83
|
+
|
|
84
|
+
_, y_stride = resample_plot(X, Y, 80, 40)
|
|
85
|
+
_, y_minmax = resample_plot_minmax(X, Y, 80, 40)
|
|
86
|
+
|
|
87
|
+
assert 1000.0 not in y_stride
|
|
88
|
+
assert 1000.0 in y_minmax
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_minmax_constant_data_dedups():
|
|
92
|
+
# min and max coincide in every bucket -> one point per bucket.
|
|
93
|
+
r = 10000
|
|
94
|
+
X = list(range(r))
|
|
95
|
+
Y = [5.0] * r
|
|
96
|
+
xm, ym = resample_plot_minmax(X, Y, 80, 40)
|
|
97
|
+
assert len(xm) == 80 * 4 // 2
|
|
98
|
+
assert set(ym) == {5.0}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_lttb_size_exact():
|
|
102
|
+
r = 10000
|
|
103
|
+
X = list(range(r))
|
|
104
|
+
Y = [math.sin(i / 100) * 100 for i in range(r)]
|
|
105
|
+
xl, yl = resample_plot_lttb(X, Y, 80, 40)
|
|
106
|
+
assert len(xl) == len(yl) == 80 * 4
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_lttb_passthrough_short_input():
|
|
110
|
+
X = [1.0, 2.0]
|
|
111
|
+
Y = [3.0, 4.0]
|
|
112
|
+
assert resample_plot_lttb(X, Y, 80, 40) == (X, Y)
|
|
113
|
+
assert resample_plot_lttb([], [], 80, 40) == ([], [])
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_lttb_keeps_endpoints_and_sorted_x():
|
|
117
|
+
r = 10000
|
|
118
|
+
X = list(range(r))
|
|
119
|
+
Y = [math.sin(i / 7) * 100 for i in range(r)]
|
|
120
|
+
xl, yl = resample_plot_lttb(X, Y, 80, 40)
|
|
121
|
+
assert xl[0] == X[0] and yl[0] == Y[0]
|
|
122
|
+
assert xl[-1] == X[-1] and yl[-1] == Y[-1]
|
|
123
|
+
assert all(a <= b for a, b in zip(xl, xl[1:]))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_lttb_keeps_a_solitary_spike():
|
|
127
|
+
r = 10000
|
|
128
|
+
X = list(range(r))
|
|
129
|
+
Y = [0.0] * r
|
|
130
|
+
Y[33] = 1000.0
|
|
131
|
+
_, yl = resample_plot_lttb(X, Y, 80, 40)
|
|
132
|
+
assert 1000.0 in yl
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_lttb_drops_one_of_two_opposing_extremes():
|
|
136
|
+
# The documented trade-off: with a peak and a valley falling in
|
|
137
|
+
# the same bucket, LTTB keeps exactly one shape point while
|
|
138
|
+
# minmax keeps both extremes.
|
|
139
|
+
r = 10000
|
|
140
|
+
X = list(range(r))
|
|
141
|
+
Y = [0.0] * r
|
|
142
|
+
Y[33] = 1000.0
|
|
143
|
+
Y[40] = -1000.0
|
|
144
|
+
|
|
145
|
+
_, yl = resample_plot_lttb(X, Y, 80, 40)
|
|
146
|
+
_, ym = resample_plot_minmax(X, Y, 80, 40)
|
|
147
|
+
|
|
148
|
+
assert (1000.0 in yl) != (-1000.0 in yl)
|
|
149
|
+
assert 1000.0 in ym and -1000.0 in ym
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_minmax_lttb_size_endpoints_and_sorted_x():
|
|
153
|
+
r = 10000
|
|
154
|
+
X = list(range(r))
|
|
155
|
+
Y = [math.sin(i / 100) * 100 for i in range(r)]
|
|
156
|
+
xh, yh = resample_plot_minmax_lttb(X, Y, 80, 40)
|
|
157
|
+
assert len(xh) == len(yh) == 80 * 4
|
|
158
|
+
assert xh[0] == X[0] and yh[0] == Y[0]
|
|
159
|
+
assert xh[-1] == X[-1] and yh[-1] == Y[-1]
|
|
160
|
+
assert all(a <= b for a, b in zip(xh, xh[1:]))
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_minmax_lttb_passthrough_short_input():
|
|
164
|
+
X = [1.0, 2.0]
|
|
165
|
+
Y = [3.0, 4.0]
|
|
166
|
+
assert resample_plot_minmax_lttb(X, Y, 80, 40) == (X, Y)
|
|
167
|
+
assert resample_plot_minmax_lttb([], [], 80, 40) == ([], [])
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_minmax_lttb_keeps_a_solitary_spike():
|
|
171
|
+
r = 10000
|
|
172
|
+
X = list(range(r))
|
|
173
|
+
Y = [0.0] * r
|
|
174
|
+
Y[33] = 1000.0
|
|
175
|
+
_, yh = resample_plot_minmax_lttb(X, Y, 80, 40)
|
|
176
|
+
assert 1000.0 in yh
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_minmax_lttb_matches_pure_lttb_below_ratio_threshold():
|
|
180
|
+
# With n_out < len(X) <= n_out * minmax_ratio there is no
|
|
181
|
+
# preselection, so hybrid and pure LTTB must return the same points.
|
|
182
|
+
r = 1000
|
|
183
|
+
X = list(range(r))
|
|
184
|
+
Y = [math.sin(i / 7) * 100 for i in range(r)]
|
|
185
|
+
assert (
|
|
186
|
+
resample_plot_minmax_lttb(X, Y, 80, 40)
|
|
187
|
+
== resample_plot_lttb(X, Y, 80, 40)
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _raises_value_error(fn, *args):
|
|
192
|
+
try:
|
|
193
|
+
fn(*args)
|
|
194
|
+
except ValueError:
|
|
195
|
+
return True
|
|
196
|
+
return False
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def test_length_mismatch_raises():
|
|
200
|
+
X = list(range(1000))
|
|
201
|
+
Y = list(range(500))
|
|
202
|
+
for fn in (
|
|
203
|
+
resample_plot, resample_plot_lttb, resample_plot_minmax,
|
|
204
|
+
resample_plot_minmax_lttb, resample_scatter,
|
|
205
|
+
):
|
|
206
|
+
assert _raises_value_error(fn, X, Y, 80, 40), fn.__name__
|
|
207
|
+
# Fail fast even when the input is below the resampling threshold.
|
|
208
|
+
assert _raises_value_error(resample_plot, [1.0], [1.0, 2.0], 80, 40)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def test_non_positive_width_raises():
|
|
212
|
+
X = list(range(1000))
|
|
213
|
+
for fn in (
|
|
214
|
+
resample_plot, resample_plot_lttb, resample_plot_minmax,
|
|
215
|
+
resample_plot_minmax_lttb, resample_scatter,
|
|
216
|
+
):
|
|
217
|
+
for w in (0, -5):
|
|
218
|
+
assert _raises_value_error(fn, X, X, w, 40), fn.__name__
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_non_positive_height_raises_only_in_scatter():
|
|
222
|
+
X = list(range(1000))
|
|
223
|
+
for h in (0, -5):
|
|
224
|
+
assert _raises_value_error(resample_scatter, X, X, 80, h)
|
|
225
|
+
# height is unused in the plot resamplers, so it stays permissive.
|
|
226
|
+
assert not _raises_value_error(resample_plot, X, X, 80, h)
|
|
227
|
+
assert not _raises_value_error(resample_plot_lttb, X, X, 80, h)
|
|
228
|
+
assert not _raises_value_error(resample_plot_minmax, X, X, 80, h)
|
|
229
|
+
assert not _raises_value_error(resample_plot_minmax_lttb, X, X, 80, h)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
if __name__ == "__main__":
|
|
233
|
+
tests = [
|
|
234
|
+
fn for name, fn in sorted(globals().items())
|
|
235
|
+
if name.startswith("test_")
|
|
236
|
+
]
|
|
237
|
+
for fn in tests:
|
|
238
|
+
fn()
|
|
239
|
+
print(f"{fn.__name__} OK")
|
|
240
|
+
print(f"--- {len(tests)} tests OK ---")
|
plotilleresample-0.4/PKG-INFO
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: plotilleresample
|
|
3
|
-
Version: 0.4
|
|
4
|
-
Summary: Python3 module to resample datasetsbefore plotting with Plotille.
|
|
5
|
-
Home-page: https://github.com/carlosplanchon/plotilleresample
|
|
6
|
-
Author: Carlos A. Planchón
|
|
7
|
-
Author-email: bubbledoloresuruguay2@gmail.com
|
|
8
|
-
License: GPL3
|
|
9
|
-
Download-URL: https://github.com/carlosplanchon/plotilleresample/archive/v0.4.tar.gz
|
|
10
|
-
Description: # plotilleresample
|
|
11
|
-
*Python3 module to resample datasets before plotting with Plotille.*
|
|
12
|
-
|
|
13
|
-
# Rationale
|
|
14
|
-
I want to optimize plot and scatter function of plotille.
|
|
15
|
-
|
|
16
|
-
## Installation
|
|
17
|
-
### Install with pip
|
|
18
|
-
```
|
|
19
|
-
pip3 install -U servusresample
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
## Usage
|
|
23
|
-
```
|
|
24
|
-
#!/usr/bin/env python3
|
|
25
|
-
|
|
26
|
-
import plotille
|
|
27
|
-
|
|
28
|
-
import plotilledimreduction
|
|
29
|
-
|
|
30
|
-
import math
|
|
31
|
-
|
|
32
|
-
from shutil import get_terminal_size
|
|
33
|
-
|
|
34
|
-
from vtclear import clear_screen
|
|
35
|
-
|
|
36
|
-
import numpy as np
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
w = get_terminal_size().columns - 20
|
|
40
|
-
h = get_terminal_size().lines - 7
|
|
41
|
-
|
|
42
|
-
r = 10000
|
|
43
|
-
res = np.random.normal(size=r)
|
|
44
|
-
|
|
45
|
-
# Here I'm testing stuffs with histograms.
|
|
46
|
-
# input("Histogram:")
|
|
47
|
-
# print(plotille.histogram(res, bins=w*2, width=w, height=h))
|
|
48
|
-
|
|
49
|
-
X = [i for i in range(r)]
|
|
50
|
-
Y = [math.sin(i / 100) * 100 for i in range(r)]
|
|
51
|
-
|
|
52
|
-
print(" · Scatter...")
|
|
53
|
-
xs, ys = plotilledimreduction.dim_reduction_scatter(X, Y, w, h)
|
|
54
|
-
print(" · Plot...")
|
|
55
|
-
xp, yp = plotilledimreduction.dim_reduction_plot(X, Y, w, h)
|
|
56
|
-
print(" --- READY ---")
|
|
57
|
-
|
|
58
|
-
print(f"Len plot.x {len(xp)}")
|
|
59
|
-
|
|
60
|
-
print(f"Len scatter.x {len(xs)}")
|
|
61
|
-
|
|
62
|
-
input("Plot:")
|
|
63
|
-
clear_screen()
|
|
64
|
-
print(plotille.plot(xp, yp, w, h))
|
|
65
|
-
|
|
66
|
-
input("Scatter:")
|
|
67
|
-
clear_screen()
|
|
68
|
-
print(plotille.plot(xs, ys, w, h))
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
Keywords: plotting,ascii,math,resample
|
|
72
|
-
Platform: UNKNOWN
|
|
73
|
-
Classifier: Intended Audience :: Developers
|
|
74
|
-
Classifier: Topic :: Software Development :: Build Tools
|
|
75
|
-
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
76
|
-
Classifier: Programming Language :: Python :: 3
|
|
77
|
-
Classifier: Programming Language :: Python :: 3.4
|
|
78
|
-
Classifier: Programming Language :: Python :: 3.5
|
|
79
|
-
Classifier: Programming Language :: Python :: 3.6
|
|
80
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
81
|
-
Description-Content-Type: text/markdown
|
plotilleresample-0.4/README.md
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
# plotilleresample
|
|
2
|
-
*Python3 module to resample datasets before plotting with Plotille.*
|
|
3
|
-
|
|
4
|
-
# Rationale
|
|
5
|
-
I want to optimize plot and scatter function of plotille.
|
|
6
|
-
|
|
7
|
-
## Installation
|
|
8
|
-
### Install with pip
|
|
9
|
-
```
|
|
10
|
-
pip3 install -U servusresample
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
## Usage
|
|
14
|
-
```
|
|
15
|
-
#!/usr/bin/env python3
|
|
16
|
-
|
|
17
|
-
import plotille
|
|
18
|
-
|
|
19
|
-
import plotilledimreduction
|
|
20
|
-
|
|
21
|
-
import math
|
|
22
|
-
|
|
23
|
-
from shutil import get_terminal_size
|
|
24
|
-
|
|
25
|
-
from vtclear import clear_screen
|
|
26
|
-
|
|
27
|
-
import numpy as np
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
w = get_terminal_size().columns - 20
|
|
31
|
-
h = get_terminal_size().lines - 7
|
|
32
|
-
|
|
33
|
-
r = 10000
|
|
34
|
-
res = np.random.normal(size=r)
|
|
35
|
-
|
|
36
|
-
# Here I'm testing stuffs with histograms.
|
|
37
|
-
# input("Histogram:")
|
|
38
|
-
# print(plotille.histogram(res, bins=w*2, width=w, height=h))
|
|
39
|
-
|
|
40
|
-
X = [i for i in range(r)]
|
|
41
|
-
Y = [math.sin(i / 100) * 100 for i in range(r)]
|
|
42
|
-
|
|
43
|
-
print(" · Scatter...")
|
|
44
|
-
xs, ys = plotilledimreduction.dim_reduction_scatter(X, Y, w, h)
|
|
45
|
-
print(" · Plot...")
|
|
46
|
-
xp, yp = plotilledimreduction.dim_reduction_plot(X, Y, w, h)
|
|
47
|
-
print(" --- READY ---")
|
|
48
|
-
|
|
49
|
-
print(f"Len plot.x {len(xp)}")
|
|
50
|
-
|
|
51
|
-
print(f"Len scatter.x {len(xs)}")
|
|
52
|
-
|
|
53
|
-
input("Plot:")
|
|
54
|
-
clear_screen()
|
|
55
|
-
print(plotille.plot(xp, yp, w, h))
|
|
56
|
-
|
|
57
|
-
input("Scatter:")
|
|
58
|
-
clear_screen()
|
|
59
|
-
print(plotille.plot(xs, ys, w, h))
|
|
60
|
-
```
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
|
|
3
|
-
from typing import List, Tuple
|
|
4
|
-
|
|
5
|
-
# This value is used to avoid Nyquist related problems.
|
|
6
|
-
plot_multiplier = 4
|
|
7
|
-
|
|
8
|
-
list_val = List[int]
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def resample_plot(
|
|
12
|
-
X: list_val,
|
|
13
|
-
Y: list_val,
|
|
14
|
-
width: int = 80,
|
|
15
|
-
height: int = 40
|
|
16
|
-
) -> Tuple[list_val, list_val]:
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
:param X: list_val: List of X values.
|
|
20
|
-
:param Y: list_val: List of Y values.
|
|
21
|
-
:param width: int: Width of the plot. (Default value = 80)
|
|
22
|
-
:param height: int: Height of the plot. (Default value = 40)
|
|
23
|
-
|
|
24
|
-
"""
|
|
25
|
-
if len(X) > width * plot_multiplier:
|
|
26
|
-
step = round(len(X) / width / plot_multiplier)
|
|
27
|
-
print(step)
|
|
28
|
-
|
|
29
|
-
if step != 0:
|
|
30
|
-
X = [
|
|
31
|
-
X[i] for i in range(
|
|
32
|
-
0, len(X), step
|
|
33
|
-
)
|
|
34
|
-
]
|
|
35
|
-
Y = [
|
|
36
|
-
Y[i] for i in range(
|
|
37
|
-
0, len(Y), step
|
|
38
|
-
)
|
|
39
|
-
]
|
|
40
|
-
|
|
41
|
-
return X, Y
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def resample_scatter(
|
|
45
|
-
X: list_val,
|
|
46
|
-
Y: list_val,
|
|
47
|
-
width: int = 80,
|
|
48
|
-
height: int = 40
|
|
49
|
-
) -> Tuple[list_val, list_val]:
|
|
50
|
-
"""
|
|
51
|
-
|
|
52
|
-
:param X: list_val: List of X values.
|
|
53
|
-
:param Y: list_val: List of Y values.
|
|
54
|
-
:param width: int: Width of the plot. (Default value = 80)
|
|
55
|
-
:param height: int: Height of the plot. (Default value = 40)
|
|
56
|
-
|
|
57
|
-
"""
|
|
58
|
-
scatter_multiplier = width * 2 * height
|
|
59
|
-
if len(X) > scatter_multiplier:
|
|
60
|
-
step = round(len(X) / scatter_multiplier)
|
|
61
|
-
print(step)
|
|
62
|
-
|
|
63
|
-
if step != 0:
|
|
64
|
-
X = [
|
|
65
|
-
X[i] for i in range(
|
|
66
|
-
0, len(X), step
|
|
67
|
-
)
|
|
68
|
-
]
|
|
69
|
-
Y = [
|
|
70
|
-
Y[i] for i in range(
|
|
71
|
-
0, len(Y), step
|
|
72
|
-
)
|
|
73
|
-
]
|
|
74
|
-
|
|
75
|
-
return X, Y
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: plotilleresample
|
|
3
|
-
Version: 0.4
|
|
4
|
-
Summary: Python3 module to resample datasetsbefore plotting with Plotille.
|
|
5
|
-
Home-page: https://github.com/carlosplanchon/plotilleresample
|
|
6
|
-
Author: Carlos A. Planchón
|
|
7
|
-
Author-email: bubbledoloresuruguay2@gmail.com
|
|
8
|
-
License: GPL3
|
|
9
|
-
Download-URL: https://github.com/carlosplanchon/plotilleresample/archive/v0.4.tar.gz
|
|
10
|
-
Description: # plotilleresample
|
|
11
|
-
*Python3 module to resample datasets before plotting with Plotille.*
|
|
12
|
-
|
|
13
|
-
# Rationale
|
|
14
|
-
I want to optimize plot and scatter function of plotille.
|
|
15
|
-
|
|
16
|
-
## Installation
|
|
17
|
-
### Install with pip
|
|
18
|
-
```
|
|
19
|
-
pip3 install -U servusresample
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
## Usage
|
|
23
|
-
```
|
|
24
|
-
#!/usr/bin/env python3
|
|
25
|
-
|
|
26
|
-
import plotille
|
|
27
|
-
|
|
28
|
-
import plotilledimreduction
|
|
29
|
-
|
|
30
|
-
import math
|
|
31
|
-
|
|
32
|
-
from shutil import get_terminal_size
|
|
33
|
-
|
|
34
|
-
from vtclear import clear_screen
|
|
35
|
-
|
|
36
|
-
import numpy as np
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
w = get_terminal_size().columns - 20
|
|
40
|
-
h = get_terminal_size().lines - 7
|
|
41
|
-
|
|
42
|
-
r = 10000
|
|
43
|
-
res = np.random.normal(size=r)
|
|
44
|
-
|
|
45
|
-
# Here I'm testing stuffs with histograms.
|
|
46
|
-
# input("Histogram:")
|
|
47
|
-
# print(plotille.histogram(res, bins=w*2, width=w, height=h))
|
|
48
|
-
|
|
49
|
-
X = [i for i in range(r)]
|
|
50
|
-
Y = [math.sin(i / 100) * 100 for i in range(r)]
|
|
51
|
-
|
|
52
|
-
print(" · Scatter...")
|
|
53
|
-
xs, ys = plotilledimreduction.dim_reduction_scatter(X, Y, w, h)
|
|
54
|
-
print(" · Plot...")
|
|
55
|
-
xp, yp = plotilledimreduction.dim_reduction_plot(X, Y, w, h)
|
|
56
|
-
print(" --- READY ---")
|
|
57
|
-
|
|
58
|
-
print(f"Len plot.x {len(xp)}")
|
|
59
|
-
|
|
60
|
-
print(f"Len scatter.x {len(xs)}")
|
|
61
|
-
|
|
62
|
-
input("Plot:")
|
|
63
|
-
clear_screen()
|
|
64
|
-
print(plotille.plot(xp, yp, w, h))
|
|
65
|
-
|
|
66
|
-
input("Scatter:")
|
|
67
|
-
clear_screen()
|
|
68
|
-
print(plotille.plot(xs, ys, w, h))
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
Keywords: plotting,ascii,math,resample
|
|
72
|
-
Platform: UNKNOWN
|
|
73
|
-
Classifier: Intended Audience :: Developers
|
|
74
|
-
Classifier: Topic :: Software Development :: Build Tools
|
|
75
|
-
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
76
|
-
Classifier: Programming Language :: Python :: 3
|
|
77
|
-
Classifier: Programming Language :: Python :: 3.4
|
|
78
|
-
Classifier: Programming Language :: Python :: 3.5
|
|
79
|
-
Classifier: Programming Language :: Python :: 3.6
|
|
80
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
81
|
-
Description-Content-Type: text/markdown
|
plotilleresample-0.4/setup.py
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
|
|
3
|
-
from setuptools import setup
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
with open("README.md", "r") as f:
|
|
7
|
-
readme = f.read()
|
|
8
|
-
|
|
9
|
-
setup(
|
|
10
|
-
name="plotilleresample",
|
|
11
|
-
packages=["plotilleresample"],
|
|
12
|
-
version="0.4",
|
|
13
|
-
license="GPL3",
|
|
14
|
-
description="Python3 module to resample datasets"
|
|
15
|
-
"before plotting with Plotille.",
|
|
16
|
-
long_description=readme,
|
|
17
|
-
long_description_content_type="text/markdown",
|
|
18
|
-
author="Carlos A. Planchón",
|
|
19
|
-
author_email="bubbledoloresuruguay2@gmail.com",
|
|
20
|
-
url="https://github.com/carlosplanchon/plotilleresample",
|
|
21
|
-
download_url="https://github.com/carlosplanchon/"
|
|
22
|
-
"plotilleresample/archive/v0.4.tar.gz",
|
|
23
|
-
keywords=["plotting", "ascii", "math", "resample"],
|
|
24
|
-
classifiers=[
|
|
25
|
-
"Intended Audience :: Developers",
|
|
26
|
-
"Topic :: Software Development :: Build Tools",
|
|
27
|
-
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
|
28
|
-
"Programming Language :: Python :: 3",
|
|
29
|
-
"Programming Language :: Python :: 3.4",
|
|
30
|
-
"Programming Language :: Python :: 3.5",
|
|
31
|
-
"Programming Language :: Python :: 3.6",
|
|
32
|
-
"Programming Language :: Python :: 3.7",
|
|
33
|
-
],
|
|
34
|
-
)
|
{plotilleresample-0.4 → plotilleresample-1.0}/plotilleresample.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|