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 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)
@@ -0,0 +1 @@
1
+ """This file makes the subdirectory a package for `resources.path`."""