uplot-python 0.0.1__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.
- uplot/__init__.py +14 -0
- uplot/color_picker.py +47 -0
- uplot/exceptions.py +12 -0
- uplot/generate_html.py +228 -0
- uplot/plot.py +36 -0
- uplot/uplot/__init__.py +1 -0
- uplot/uplot/uPlot.iife.js +5029 -0
- uplot/uplot/uPlot.min.css +1 -0
- uplot/uplot/uPlot.mousewheel.js +126 -0
- uplot/write_tmpfile.py +32 -0
- uplot_python-0.0.1.dist-info/LICENSE +201 -0
- uplot_python-0.0.1.dist-info/METADATA +31 -0
- uplot_python-0.0.1.dist-info/RECORD +14 -0
- uplot_python-0.0.1.dist-info/WHEEL +4 -0
uplot/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
# Copyright 2022 Stéphane Caron
|
|
6
|
+
# Copyright 2023-2024 Inria
|
|
7
|
+
|
|
8
|
+
"""Plot Python iterables with µPlot."""
|
|
9
|
+
|
|
10
|
+
__version__ = "0.0.1"
|
|
11
|
+
|
|
12
|
+
from .plot import plot
|
|
13
|
+
|
|
14
|
+
__all__ = ["plot"]
|
uplot/color_picker.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
# Copyright 2022 Stéphane Caron
|
|
6
|
+
# Copyright 2023-2024 Inria
|
|
7
|
+
|
|
8
|
+
"""Pick plot colors from a circular list."""
|
|
9
|
+
|
|
10
|
+
from typing import List
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ColorPicker:
|
|
14
|
+
"""Pick from a circular list of color strings."""
|
|
15
|
+
|
|
16
|
+
COLORS: List[str] = [
|
|
17
|
+
"red",
|
|
18
|
+
"green",
|
|
19
|
+
"blue",
|
|
20
|
+
"magenta",
|
|
21
|
+
"orange",
|
|
22
|
+
"cyan",
|
|
23
|
+
"purple",
|
|
24
|
+
"lime",
|
|
25
|
+
"#AABBCC",
|
|
26
|
+
"#BBAACC",
|
|
27
|
+
"#CCBBAA",
|
|
28
|
+
"#AABBAA",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
def __init__(self):
|
|
32
|
+
"""Initialize color picker."""
|
|
33
|
+
self.reset()
|
|
34
|
+
|
|
35
|
+
def get_next_color(self) -> str:
|
|
36
|
+
"""Get next color in the list.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Color string.
|
|
40
|
+
"""
|
|
41
|
+
color = self.COLORS[self.__next_color]
|
|
42
|
+
self.__next_color += 1
|
|
43
|
+
return color
|
|
44
|
+
|
|
45
|
+
def reset(self) -> None:
|
|
46
|
+
"""Reset picker to the first color."""
|
|
47
|
+
self.__next_color = 0
|
uplot/exceptions.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
# Copyright 2022 Stéphane Caron
|
|
6
|
+
# Copyright 2024 Inria
|
|
7
|
+
|
|
8
|
+
"""Project exceptions."""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class UplotException(Exception):
|
|
12
|
+
"""Base class for uPlot exceptions."""
|
uplot/generate_html.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
# Copyright 2022 Stéphane Caron
|
|
6
|
+
# Copyright 2023 Inria
|
|
7
|
+
|
|
8
|
+
"""Generate an HTML page containing the output plot."""
|
|
9
|
+
|
|
10
|
+
from importlib import resources
|
|
11
|
+
from math import isnan
|
|
12
|
+
from typing import Dict, Iterable, List
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
from numpy.typing import NDArray
|
|
16
|
+
|
|
17
|
+
from .color_picker import ColorPicker
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def __ensure_floats(series: Iterable) -> List[float]:
|
|
21
|
+
return [float(x) for x in series]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def __escape_null(series: Iterable) -> str:
|
|
25
|
+
"""Escape undefined values in a series.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
series: Series to filter.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
String representation of the series.
|
|
32
|
+
"""
|
|
33
|
+
return (
|
|
34
|
+
"["
|
|
35
|
+
+ ", ".join(
|
|
36
|
+
map(
|
|
37
|
+
lambda x: (
|
|
38
|
+
str(int(x))
|
|
39
|
+
if isinstance(x, bool)
|
|
40
|
+
else (
|
|
41
|
+
str(x)
|
|
42
|
+
if isinstance(x, (int, float)) and not isnan(x)
|
|
43
|
+
else x if isinstance(x, str) else "null"
|
|
44
|
+
)
|
|
45
|
+
),
|
|
46
|
+
series,
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
+ "]"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def generate_html(
|
|
54
|
+
opts: dict,
|
|
55
|
+
data: List[Iterable[float, int]],
|
|
56
|
+
) -> str:
|
|
57
|
+
"""Generate plot in an HTML page.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
HTML contents of the page.
|
|
61
|
+
"""
|
|
62
|
+
with resources.path("foxplot.uplot", "uPlot.min.css") as path:
|
|
63
|
+
uplot_min_css = path
|
|
64
|
+
with resources.path("foxplot.uplot", "uPlot.iife.js") as path:
|
|
65
|
+
uplot_iife_js = path
|
|
66
|
+
with resources.path("foxplot.uplot", "uPlot.mousewheel.js") as path:
|
|
67
|
+
uplot_mwheel_js = path
|
|
68
|
+
|
|
69
|
+
color_picker = ColorPicker()
|
|
70
|
+
right_axis_label = f" {right_axis_unit}" if right_axis_unit else ""
|
|
71
|
+
left_labels = list(left_axis.keys())
|
|
72
|
+
right_labels = list(right_axis.keys())
|
|
73
|
+
labels = left_labels + [r for r in right_labels if r not in left_labels]
|
|
74
|
+
series_from_label = {}
|
|
75
|
+
series_from_label.update(left_axis)
|
|
76
|
+
series_from_label.update(right_axis)
|
|
77
|
+
html = f"""<!DOCTYPE html>
|
|
78
|
+
<html lang="en">
|
|
79
|
+
<head>
|
|
80
|
+
<meta charset="utf-8">
|
|
81
|
+
<title>{title}</title>
|
|
82
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
83
|
+
<link rel="stylesheet" href="{uplot_min_css}">
|
|
84
|
+
<style>
|
|
85
|
+
div.my-chart {{
|
|
86
|
+
background-color: white;
|
|
87
|
+
padding: 10px 0px; // appear in Right Click -> Take Screenshot
|
|
88
|
+
}}
|
|
89
|
+
</style>
|
|
90
|
+
</head>
|
|
91
|
+
<body>
|
|
92
|
+
<script src="{uplot_iife_js}"></script>
|
|
93
|
+
<script src="{uplot_mwheel_js}"></script>
|
|
94
|
+
<script>
|
|
95
|
+
const {{ linear, stepped, bars, spline, spline2 }} = uPlot.paths;
|
|
96
|
+
|
|
97
|
+
let data = ["""
|
|
98
|
+
for label in data.keys():
|
|
99
|
+
html += f"""
|
|
100
|
+
{__escape_null(data[label])},"""
|
|
101
|
+
html += """
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
const lineInterpolations = {
|
|
105
|
+
linear: 0,
|
|
106
|
+
stepAfter: 1,
|
|
107
|
+
stepBefore: 2,
|
|
108
|
+
spline: 3,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const _stepBefore = stepped({align: -1});
|
|
112
|
+
const _stepAfter = stepped({align: 1});
|
|
113
|
+
const _linear = linear();
|
|
114
|
+
const _spline = spline();
|
|
115
|
+
|
|
116
|
+
function paths(u, seriesIdx, idx0, idx1, extendGap, buildClip) {
|
|
117
|
+
let s = u.series[seriesIdx];
|
|
118
|
+
let interp = s.lineInterpolation;
|
|
119
|
+
|
|
120
|
+
let renderer = (
|
|
121
|
+
interp == lineInterpolations.linear ? _linear :
|
|
122
|
+
interp == lineInterpolations.stepAfter ? _stepAfter :
|
|
123
|
+
interp == lineInterpolations.stepBefore ? _stepBefore :
|
|
124
|
+
interp == lineInterpolations.spline ? _spline :
|
|
125
|
+
null
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
return renderer(
|
|
129
|
+
u, seriesIdx, idx0, idx1, extendGap, buildClip
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let opts = {"""
|
|
134
|
+
html += f"""
|
|
135
|
+
title: "{title}","""
|
|
136
|
+
html += """
|
|
137
|
+
id: "chart1",
|
|
138
|
+
class: "my-chart",
|
|
139
|
+
width: window.innerWidth - 20,
|
|
140
|
+
height: window.innerHeight - 150,
|
|
141
|
+
cursor: {
|
|
142
|
+
drag: {
|
|
143
|
+
x: true,
|
|
144
|
+
y: true,
|
|
145
|
+
uni: 50,
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
plugins: [
|
|
149
|
+
wheelZoomPlugin({factor: 0.75})
|
|
150
|
+
],"""
|
|
151
|
+
html += f"""
|
|
152
|
+
scales: {{
|
|
153
|
+
x: {{
|
|
154
|
+
time: {"true" if timestamped else "false"},
|
|
155
|
+
}},
|
|
156
|
+
}},
|
|
157
|
+
series: ["""
|
|
158
|
+
html += f"""
|
|
159
|
+
{{
|
|
160
|
+
value: (self, rawValue) => Number.parseFloat(rawValue -
|
|
161
|
+
{times[0]}).toPrecision(4),
|
|
162
|
+
}},"""
|
|
163
|
+
for label in labels:
|
|
164
|
+
html += f"""
|
|
165
|
+
{{
|
|
166
|
+
// initial toggled state (optional)
|
|
167
|
+
show: true,
|
|
168
|
+
spanGaps: false,
|
|
169
|
+
|
|
170
|
+
// in-legend display
|
|
171
|
+
label: "{label}","""
|
|
172
|
+
if label in right_labels:
|
|
173
|
+
html += f"""
|
|
174
|
+
value: (self, rawValue) =>
|
|
175
|
+
Number.parseFloat(rawValue).toPrecision(2) +
|
|
176
|
+
"{right_axis_label}",
|
|
177
|
+
scale: "{right_axis_unit}","""
|
|
178
|
+
else: # label in left_labels
|
|
179
|
+
html += f"""
|
|
180
|
+
value: (self, rawValue) =>
|
|
181
|
+
Number.parseFloat(rawValue).toPrecision(2) +
|
|
182
|
+
"{left_axis_label}","""
|
|
183
|
+
html += f"""
|
|
184
|
+
// series style
|
|
185
|
+
stroke: "{color_picker.get_next_color()}",
|
|
186
|
+
width: 2 / devicePixelRatio,
|
|
187
|
+
lineInterpolation: lineInterpolations.stepAfter,
|
|
188
|
+
paths,
|
|
189
|
+
}},"""
|
|
190
|
+
html += """
|
|
191
|
+
],
|
|
192
|
+
axes: [
|
|
193
|
+
{},
|
|
194
|
+
{"""
|
|
195
|
+
html += f"""
|
|
196
|
+
size: {60 + 10 * len({left_axis_label})},
|
|
197
|
+
values: (u, vals, space) => vals.map(
|
|
198
|
+
v => v + "{left_axis_label}"
|
|
199
|
+
),"""
|
|
200
|
+
html += """
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
side: 1,"""
|
|
204
|
+
html += f"""
|
|
205
|
+
scale: "{right_axis_unit}",
|
|
206
|
+
size: {60 + 10 * len({right_axis_label})},
|
|
207
|
+
values: (u, vals, space) => vals.map(
|
|
208
|
+
v => v + "{right_axis_label}"
|
|
209
|
+
),"""
|
|
210
|
+
html += """
|
|
211
|
+
grid: {show: false},
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
let uplot = new uPlot(opts, data, document.body);
|
|
217
|
+
|
|
218
|
+
// resize with window
|
|
219
|
+
window.addEventListener("resize", e => {
|
|
220
|
+
uplot.setSize({
|
|
221
|
+
width: window.innerWidth - 20,
|
|
222
|
+
height: window.innerHeight - 150,
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
</script>
|
|
226
|
+
</body>
|
|
227
|
+
</html>"""
|
|
228
|
+
return html
|
uplot/plot.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
# Copyright 2024 Inria
|
|
6
|
+
|
|
7
|
+
"""Main class to manipulate dictionary-series data."""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import sys
|
|
11
|
+
import webbrowser
|
|
12
|
+
from typing import BinaryIO, Dict, List, Optional, TextIO, Union
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
from numpy.typing import NDArray
|
|
16
|
+
|
|
17
|
+
from .generate_html import generate_html
|
|
18
|
+
from .write_tmpfile import write_tmpfile
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def plot(
|
|
22
|
+
self,
|
|
23
|
+
opts: dict,
|
|
24
|
+
data: List[Iterable[float, int]],
|
|
25
|
+
) -> None:
|
|
26
|
+
html = generate_html(
|
|
27
|
+
times,
|
|
28
|
+
left_series,
|
|
29
|
+
right_series,
|
|
30
|
+
title,
|
|
31
|
+
left_axis_unit,
|
|
32
|
+
right_axis_unit,
|
|
33
|
+
timestamped=self.__time is not None,
|
|
34
|
+
)
|
|
35
|
+
filename = write_tmpfile(html)
|
|
36
|
+
webbrowser.open_new_tab(filename)
|
uplot/uplot/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""This file makes the subdirectory a package for `resources.path`."""
|