uplot-python 0.0.1__tar.gz → 1.0.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.
Files changed (27) hide show
  1. {uplot_python-0.0.1 → uplot_python-1.0.0}/CHANGELOG.md +1 -1
  2. uplot_python-1.0.0/MANIFEST.in +1 -0
  3. {uplot_python-0.0.1 → uplot_python-1.0.0}/PKG-INFO +51 -2
  4. uplot_python-1.0.0/README.md +52 -0
  5. uplot_python-1.0.0/examples/left_right_axes.py +159 -0
  6. uplot_python-1.0.0/examples/simple_plot.py +34 -0
  7. uplot_python-1.0.0/examples/simple_plot2.py +18 -0
  8. {uplot_python-0.0.1 → uplot_python-1.0.0}/uplot/__init__.py +7 -3
  9. uplot_python-1.0.0/uplot/generate_html.py +121 -0
  10. uplot_python-1.0.0/uplot/plot.py +25 -0
  11. uplot_python-1.0.0/uplot/plot2.py +161 -0
  12. uplot_python-1.0.0/uplot/utils.py +37 -0
  13. uplot_python-0.0.1/uplot/write_tmpfile.py → uplot_python-1.0.0/uplot/write_html_tempfile.py +3 -3
  14. uplot_python-0.0.1/MANIFEST.in +0 -1
  15. uplot_python-0.0.1/README.md +0 -3
  16. uplot_python-0.0.1/examples/simple_plot.py +0 -12
  17. uplot_python-0.0.1/uplot/generate_html.py +0 -228
  18. uplot_python-0.0.1/uplot/plot.py +0 -36
  19. {uplot_python-0.0.1 → uplot_python-1.0.0}/.gitignore +0 -0
  20. {uplot_python-0.0.1 → uplot_python-1.0.0}/LICENSE +0 -0
  21. {uplot_python-0.0.1 → uplot_python-1.0.0}/pyproject.toml +0 -0
  22. {uplot_python-0.0.1 → uplot_python-1.0.0}/uplot/color_picker.py +0 -0
  23. {uplot_python-0.0.1 → uplot_python-1.0.0}/uplot/exceptions.py +0 -0
  24. {uplot_python-0.0.1/uplot/uplot → uplot_python-1.0.0/uplot/static}/__init__.py +0 -0
  25. {uplot_python-0.0.1/uplot/uplot → uplot_python-1.0.0/uplot/static}/uPlot.iife.js +0 -0
  26. {uplot_python-0.0.1/uplot/uplot → uplot_python-1.0.0/uplot/static}/uPlot.min.css +0 -0
  27. {uplot_python-0.0.1/uplot/uplot → uplot_python-1.0.0/uplot/static}/uPlot.mousewheel.js +0 -0
@@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- ## [0.0.1] - 2024-10-29
10
+ ## [1.0.0] - 2024-10-29
11
11
 
12
12
  - Extract this project from [foxplot](https://github.com/stephane-caron/foxplot)
13
13
  - Start this changelog
@@ -0,0 +1 @@
1
+ include uplot/static/uPlot.*
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: uplot-python
3
- Version: 0.0.1
4
- Summary: Plot Python iterables with µPlot.
3
+ Version: 1.0.0
4
+ Summary: Python wrapper for μPlot time series.
5
5
  Keywords: json,time,series,plot
6
6
  Author-email: Stéphane Caron <stephane.caron@normalesup.org>
7
7
  Maintainer-email: Stéphane Caron <stephane.caron@normalesup.org>
@@ -29,3 +29,52 @@ Project-URL: Tracker, https://github.com/stephane-caron/uplot-python/issues
29
29
 
30
30
  Python wrapper for [μPlot](https://github.com/leeoniya/uPlot) 📈
31
31
 
32
+ ## Installation
33
+
34
+ ### From PyPI
35
+
36
+ ```console
37
+ pip install uplot-python
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ The `plot` function has the same API as µPlot's `uPlot.plot`:
43
+
44
+ ```py
45
+ import numpy as np
46
+ import uplot
47
+
48
+ t = np.linspace(0.0, 1.0, 10)
49
+ data = [t, np.exp(0.42 * t)]
50
+ opts = {
51
+ "width": 1920,
52
+ "height": 600,
53
+ "title": "Example with plot",
54
+ "series": [{}, { "stroke": "red", }, ],
55
+ }
56
+
57
+ uplot.plot(opts, data)
58
+ ```
59
+
60
+ For convenience, the library also provides a `plot2` function with additional defaults aimed at time series and line plots, for an experience closer to `matplotlib.pyplot.plot`:
61
+
62
+ ```py
63
+ import numpy as np
64
+ import uplot
65
+
66
+ t = np.linspace(0.0, 1.0, 10)
67
+ uplot.plot2(
68
+ t,
69
+ [np.exp(0.1 * t), np.exp(-10.0 * t), np.cos(t)],
70
+ title="Example with plot2",
71
+ left_labels=["exp(A t)", "exp(-B t)", "cos(t)"],
72
+ )
73
+ ```
74
+
75
+ ## See also
76
+
77
+ - [µPlot](https://github.com/leeoniya/uPlot): A small (~45 KB min), fast chart for time series, lines, areas, ohlc & bars.
78
+ - [Matplotlib](https://matplotlib.org/stable/): Comprehensive library for creating static, animated, and interactive visualizations.
79
+ - [matplotlive](https://github.com/stephane-caron/matplotlive): Stream live plots to a Matplotlib figure.
80
+
@@ -0,0 +1,52 @@
1
+ # uplot-python
2
+
3
+ Python wrapper for [μPlot](https://github.com/leeoniya/uPlot) 📈
4
+
5
+ ## Installation
6
+
7
+ ### From PyPI
8
+
9
+ ```console
10
+ pip install uplot-python
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ The `plot` function has the same API as µPlot's `uPlot.plot`:
16
+
17
+ ```py
18
+ import numpy as np
19
+ import uplot
20
+
21
+ t = np.linspace(0.0, 1.0, 10)
22
+ data = [t, np.exp(0.42 * t)]
23
+ opts = {
24
+ "width": 1920,
25
+ "height": 600,
26
+ "title": "Example with plot",
27
+ "series": [{}, { "stroke": "red", }, ],
28
+ }
29
+
30
+ uplot.plot(opts, data)
31
+ ```
32
+
33
+ For convenience, the library also provides a `plot2` function with additional defaults aimed at time series and line plots, for an experience closer to `matplotlib.pyplot.plot`:
34
+
35
+ ```py
36
+ import numpy as np
37
+ import uplot
38
+
39
+ t = np.linspace(0.0, 1.0, 10)
40
+ uplot.plot2(
41
+ t,
42
+ [np.exp(0.1 * t), np.exp(-10.0 * t), np.cos(t)],
43
+ title="Example with plot2",
44
+ left_labels=["exp(A t)", "exp(-B t)", "cos(t)"],
45
+ )
46
+ ```
47
+
48
+ ## See also
49
+
50
+ - [µPlot](https://github.com/leeoniya/uPlot): A small (~45 KB min), fast chart for time series, lines, areas, ohlc & bars.
51
+ - [Matplotlib](https://matplotlib.org/stable/): Comprehensive library for creating static, animated, and interactive visualizations.
52
+ - [matplotlive](https://github.com/stephane-caron/matplotlive): Stream live plots to a Matplotlib figure.
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # SPDX-License-Identifier: Apache-2.0
5
+ # Copyright 2024 Inria
6
+
7
+ import numpy as np
8
+
9
+ import uplot
10
+
11
+ v1 = np.array(
12
+ [
13
+ 0.35189867,
14
+ 0.33440887,
15
+ 0.31187567,
16
+ 0.29372533,
17
+ 0.28205933,
18
+ 0.28579414,
19
+ 0.29055172,
20
+ 0.29449164,
21
+ 0.28745652,
22
+ 0.27642006,
23
+ 0.26409361,
24
+ 0.24920144,
25
+ 0.21968901,
26
+ 0.1903474,
27
+ 0.14416688,
28
+ 0.1013935,
29
+ 0.06975446,
30
+ 0.05133968,
31
+ 0.0492436,
32
+ 0.05916173,
33
+ 0.06069822,
34
+ 0.05255136,
35
+ 0.03641129,
36
+ 0.01232885,
37
+ -0.02340249,
38
+ -0.06951351,
39
+ -0.12411359,
40
+ -0.18078907,
41
+ -0.23323237,
42
+ -0.27583448,
43
+ -0.31259793,
44
+ -0.33245975,
45
+ -0.32182605,
46
+ -0.27193381,
47
+ -0.1870999,
48
+ -0.09126389,
49
+ -0.01173261,
50
+ 0.01668092,
51
+ 0.00449685,
52
+ -0.03245584,
53
+ -0.07189206,
54
+ -0.09525084,
55
+ ]
56
+ )
57
+ v2 = np.array(
58
+ [
59
+ 0.40215528,
60
+ 0.38603891,
61
+ 0.35140285,
62
+ 0.3215263,
63
+ 0.27873781,
64
+ 0.27077387,
65
+ 0.24593958,
66
+ 0.28406281,
67
+ 0.29895396,
68
+ 0.28722011,
69
+ 0.25734356,
70
+ 0.27006701,
71
+ 0.27350706,
72
+ 0.25635396,
73
+ 0.21898472,
74
+ 0.17379291,
75
+ 0.08331504,
76
+ 0.03039491,
77
+ -0.00202633,
78
+ -0.00801106,
79
+ 0.01757721,
80
+ 0.03845309,
81
+ 0.04877323,
82
+ 0.03812323,
83
+ 0.03562566,
84
+ 0.03322234,
85
+ -0.01041438,
86
+ -0.06894225,
87
+ -0.13345486,
88
+ -0.18703472,
89
+ -0.24862564,
90
+ -0.31921723,
91
+ -0.40314488,
92
+ -0.44697009,
93
+ -0.40724466,
94
+ -0.29188537,
95
+ -0.12044866,
96
+ 0.03199712,
97
+ 0.10329557,
98
+ 0.10706548,
99
+ 0.01050863,
100
+ -0.04231725,
101
+ ]
102
+ )
103
+ u = np.array(
104
+ [
105
+ 6.31617203,
106
+ 6.14966762,
107
+ 5.69570748,
108
+ 4.84433587,
109
+ 4.62913678,
110
+ 4.51132705,
111
+ 3.91756604,
112
+ 4.66997748,
113
+ 4.9165925,
114
+ 5.0029863,
115
+ 4.78307482,
116
+ 4.5333182,
117
+ 4.75794207,
118
+ 4.43121644,
119
+ 3.68980057,
120
+ 2.83057498,
121
+ 1.02101761,
122
+ 0.04555309,
123
+ 0.18064158,
124
+ -0.02199115,
125
+ 0.40840704,
126
+ 0.80896011,
127
+ 0.94561939,
128
+ 0.69272118,
129
+ 0.49480084,
130
+ 0.44610616,
131
+ -0.0408407,
132
+ -1.22836273,
133
+ -2.37975644,
134
+ -3.24212362,
135
+ -4.06836249,
136
+ -5.40353936,
137
+ -6.76227819,
138
+ -7.06544188,
139
+ -6.70572952,
140
+ -5.23703495,
141
+ -1.99805293,
142
+ 0.25289821,
143
+ 1.52053084,
144
+ 1.53623881,
145
+ 0.01413717,
146
+ -0.73356188,
147
+ ]
148
+ )
149
+
150
+ if __name__ == "__main__":
151
+ t = np.linspace(0.0, 1.0, len(v1))
152
+ uplot.plot2(
153
+ t,
154
+ left=[v1, v2],
155
+ right=[u],
156
+ title="Plot with left and right axes",
157
+ left_labels=["v1", "v2"],
158
+ right_labels=["u"],
159
+ )
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # SPDX-License-Identifier: Apache-2.0
5
+ # Copyright 2024 Inria
6
+
7
+ import numpy as np
8
+
9
+ import uplot
10
+
11
+ if __name__ == "__main__":
12
+ t = np.linspace(0.0, 1.0, 10)
13
+ data = [
14
+ t,
15
+ np.exp(0.42 * t),
16
+ ]
17
+ opts = {
18
+ "width": 1920,
19
+ "height": 600,
20
+ "title": "Simple plot",
21
+ "scales": {
22
+ "x": {
23
+ "time": False,
24
+ },
25
+ },
26
+ "series": [
27
+ {},
28
+ {
29
+ "stroke": "red",
30
+ "fill": "rgba(255,0,0,0.1)",
31
+ },
32
+ ],
33
+ }
34
+ uplot.plot(opts, data)
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # SPDX-License-Identifier: Apache-2.0
5
+ # Copyright 2024 Inria
6
+
7
+ import numpy as np
8
+
9
+ import uplot
10
+
11
+ if __name__ == "__main__":
12
+ t = np.linspace(0.0, 1.0, 10)
13
+ uplot.plot2(
14
+ t,
15
+ [np.exp(0.1 * t), np.exp(-10.0 * t), np.cos(t)],
16
+ title="Simple plot",
17
+ left_labels=["exp(0.1 t)", "exp(-K t)", "cos(t)"],
18
+ )
@@ -5,10 +5,14 @@
5
5
  # Copyright 2022 Stéphane Caron
6
6
  # Copyright 2023-2024 Inria
7
7
 
8
- """Plot Python iterables with µPlot."""
8
+ """Python wrapper for μPlot time series."""
9
9
 
10
- __version__ = "0.0.1"
10
+ __version__ = "1.0.0"
11
11
 
12
12
  from .plot import plot
13
+ from .plot2 import plot2
13
14
 
14
- __all__ = ["plot"]
15
+ __all__ = [
16
+ "plot",
17
+ "plot2",
18
+ ]
@@ -0,0 +1,121 @@
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
+ import json
11
+ from datetime import datetime
12
+ from importlib import resources
13
+ from typing import List
14
+
15
+ import numpy as np
16
+
17
+ from .utils import array2string, js
18
+
19
+
20
+ def generate_html(opts: dict, data: List[np.ndarray], resize: bool) -> str:
21
+ """Generate plot in an HTML page.
22
+
23
+ Args:
24
+ opts: uPlot option dictionary.
25
+ data: List of NumPy arrays, one for each series in the plot.
26
+
27
+ Returns:
28
+ HTML contents of the page.
29
+ """
30
+ with resources.path("uplot.static", "uPlot.min.css") as path:
31
+ uplot_min_css = path
32
+ with resources.path("uplot.static", "uPlot.iife.js") as path:
33
+ uplot_iife_js = path
34
+ with resources.path("uplot.static", "uPlot.mousewheel.js") as path:
35
+ uplot_mwheel_js = path
36
+
37
+ date = datetime.now().strftime("%Y%m%d-%H%M%S")
38
+ title = opts.get("title", f"Plot from {date}")
39
+
40
+ data_string = ""
41
+ for array in data:
42
+ data_string += f"""
43
+ {array2string(array)},"""
44
+
45
+ if "class" not in opts:
46
+ opts["class"] = "uplot-chart"
47
+ if resize:
48
+ opts["width"] = js("window.innerWidth - 20")
49
+ opts["height"] = js("window.innerHeight - 150")
50
+ opts_string = json.dumps(opts)
51
+ opts_string = opts_string.replace('"<script>', "")
52
+ opts_string = opts_string.replace('</script>"', "")
53
+
54
+ html = f"""<!DOCTYPE html>
55
+ <html lang="en">
56
+ <head>
57
+ <meta charset="utf-8">
58
+ <title>{title}</title>
59
+ <meta name="viewport" content="width=device-width, initial-scale=1">
60
+ <link rel="stylesheet" href="{uplot_min_css}">
61
+ <style>
62
+ div.uplot-chart {{
63
+ background-color: white;
64
+ padding: 10px 0px; // appear in Right Click -> Take Screenshot
65
+ }}
66
+ </style>
67
+ </head>
68
+ <body>
69
+ <script src="{uplot_iife_js}"></script>
70
+ <script src="{uplot_mwheel_js}"></script>
71
+ <script>
72
+ const {{ linear, stepped, bars, spline, spline2 }} = uPlot.paths;
73
+
74
+ let data = [{data_string}
75
+ ];
76
+
77
+ const lineInterpolations = {{
78
+ linear: 0,
79
+ stepAfter: 1,
80
+ stepBefore: 2,
81
+ spline: 3,
82
+ }};
83
+
84
+ const _stepBefore = stepped({{align: -1}});
85
+ const _stepAfter = stepped({{align: 1}});
86
+ const _linear = linear();
87
+ const _spline = spline();
88
+
89
+ function paths(u, seriesIdx, idx0, idx1, extendGap, buildClip) {{
90
+ let s = u.series[seriesIdx];
91
+ let interp = s.lineInterpolation;
92
+
93
+ let renderer = (
94
+ interp == lineInterpolations.linear ? _linear :
95
+ interp == lineInterpolations.stepAfter ? _stepAfter :
96
+ interp == lineInterpolations.stepBefore ? _stepBefore :
97
+ interp == lineInterpolations.spline ? _spline :
98
+ null
99
+ );
100
+
101
+ return renderer(
102
+ u, seriesIdx, idx0, idx1, extendGap, buildClip
103
+ );
104
+ }}
105
+
106
+ let opts = {opts_string};
107
+ let uplot = new uPlot(opts, data, document.body);"""
108
+ if resize:
109
+ html += """
110
+
111
+ window.addEventListener("resize", e => {
112
+ uplot.setSize({
113
+ width: window.innerWidth - 20,
114
+ height: window.innerHeight - 150,
115
+ });
116
+ });"""
117
+ html += """
118
+ </script>
119
+ </body>
120
+ </html>"""
121
+ return html
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # SPDX-License-Identifier: Apache-2.0
5
+ # Copyright 2024 Inria
6
+
7
+ """Main plot function."""
8
+
9
+ import webbrowser
10
+ from typing import Iterable, List
11
+
12
+ from .generate_html import generate_html
13
+ from .write_html_tempfile import write_html_tempfile
14
+
15
+
16
+ def plot(opts: dict, data: List[Iterable]) -> None:
17
+ """Plot function with the same API as uPlot's `plot`.
18
+
19
+ Args:
20
+ opts: Options dictionary, as expected by uPlot.
21
+ data: List of series, as expected by uPlot.
22
+ """
23
+ html = generate_html(opts, data, resize=False)
24
+ filename = write_html_tempfile(html)
25
+ webbrowser.open_new_tab(filename)
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # SPDX-License-Identifier: Apache-2.0
5
+ # Copyright 2024 Inria
6
+
7
+ """Additional plot function."""
8
+
9
+ import webbrowser
10
+ from typing import List, Optional
11
+
12
+ import numpy as np
13
+
14
+ from .color_picker import ColorPicker
15
+ from .exceptions import UplotException
16
+ from .generate_html import generate_html
17
+ from .utils import js
18
+ from .write_html_tempfile import write_html_tempfile
19
+
20
+
21
+ def prepare_data(x, left, right):
22
+ if isinstance(left, np.ndarray) and left.ndim == 1:
23
+ left = [left]
24
+ data = [x, *left]
25
+ if right:
26
+ data.extend(right)
27
+ return data
28
+
29
+
30
+ def add_default_options(opts: dict) -> None:
31
+ if "cursor" not in opts:
32
+ opts["cursor"] = {
33
+ "drag": {
34
+ "x": True,
35
+ "y": True,
36
+ "uni": 50,
37
+ }
38
+ }
39
+ if "plugins" not in opts:
40
+ opts["plugins"] = [
41
+ js("wheelZoomPlugin({factor: 0.75})"),
42
+ ]
43
+
44
+
45
+ def add_series(
46
+ opts: dict,
47
+ data: List[np.ndarray],
48
+ nb_left: int,
49
+ left_labels: Optional[List[str]],
50
+ right_labels: Optional[List[str]],
51
+ ) -> None:
52
+ if left_labels is not None and len(left_labels) < nb_left:
53
+ raise UplotException(
54
+ f"Not enough labels in left_labels ({len(left_labels)}) "
55
+ f"to label all {nb_left} left-axis series"
56
+ )
57
+
58
+ opts["series"] = [{}]
59
+ color_picker = ColorPicker()
60
+ for i, series in enumerate(data[1:]):
61
+ new_series = {
62
+ "show": True,
63
+ "spanGaps": False,
64
+ "stroke": color_picker.get_next_color(),
65
+ "width": js("2 / devicePixelRatio"),
66
+ }
67
+
68
+ def find_in_lists(
69
+ i: int,
70
+ left_list: Optional[List[str]],
71
+ right_list: Optional[List[str]],
72
+ ) -> Optional[str]:
73
+ return None
74
+
75
+ if left_labels is not None and i < nb_left:
76
+ new_series["label"] = left_labels[i]
77
+ if i >= nb_left:
78
+ if right_labels is not None:
79
+ new_series["label"] = right_labels[i - nb_left]
80
+ new_series["scale"] = "right_axis"
81
+
82
+ # scale = find_in_lists(i, left_scales, right_scales)
83
+ # if scale is not None:
84
+ # new_series["scale"] = scale
85
+
86
+ new_series["value"] = js(
87
+ "(self, rawValue) => Number.parseFloat(rawValue).toPrecision(2)"
88
+ )
89
+
90
+ # Last, we hack the wrapper to append `paths` after "lineInterpolation"
91
+ new_series["lineInterpolation"] = js(
92
+ "lineInterpolations.stepAfter, paths"
93
+ )
94
+ opts["series"].append(new_series)
95
+
96
+
97
+ def add_axes(opts: dict) -> None:
98
+ opts["axes"] = [
99
+ {},
100
+ {
101
+ "size": 60,
102
+ },
103
+ {
104
+ "side": 1,
105
+ "scale": "right_axis",
106
+ "size": 60,
107
+ "grid": {"show": False},
108
+ },
109
+ ]
110
+
111
+
112
+ def plot2(
113
+ x: np.ndarray,
114
+ left: List[np.ndarray],
115
+ right: Optional[List[np.ndarray]] = None,
116
+ resize: bool = True,
117
+ title: Optional[str] = None,
118
+ timestamped: bool = False,
119
+ width: Optional[int] = None,
120
+ height: Optional[int] = None,
121
+ left_labels: Optional[List[str]] = None,
122
+ right_labels: Optional[List[str]] = None,
123
+ **kwargs,
124
+ ) -> None:
125
+ """Plot function with additional defaults and parameters.
126
+
127
+ Args:
128
+ x: Values for the x-axis.
129
+ left: Values for the left y-axis.
130
+ right: Values for the (optional) right y-axis.
131
+ resize: If set (default), scale plot to page width and height.
132
+ title: Plot title.
133
+ timestamped: If set, x-axis values are treated as timestamps.
134
+ width: Plot width in pixels.
135
+ height: Plot height in pixels.
136
+ kwargs: Other keyword arguments are forward to uPlot as options.
137
+ """
138
+ data = prepare_data(x, left, right)
139
+
140
+ # Prepare options
141
+ opts = kwargs.copy()
142
+ add_default_options(opts)
143
+ if "id" not in opts:
144
+ opts["id"] = "chart1"
145
+ if title is not None:
146
+ opts["title"] = title
147
+ if timestamped is not None:
148
+ opts["scales"] = {"x": {"time": timestamped}}
149
+ if width is not None:
150
+ opts["width"] = width
151
+ if height is not None:
152
+ opts["height"] = height
153
+ if "series" not in opts:
154
+ add_series(opts, data, len(left), left_labels, right_labels)
155
+ if "axes" not in opts:
156
+ add_axes(opts)
157
+
158
+ # Generate and open plot
159
+ html = generate_html(opts, data, resize=resize)
160
+ filename = write_html_tempfile(html)
161
+ webbrowser.open_new_tab(filename)
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # SPDX-License-Identifier: Apache-2.0
5
+ # Copyright 2024 Inria
6
+
7
+ import numpy as np
8
+
9
+
10
+ def array2string(array: np.ndarray) -> str:
11
+ """Get string representation of a NumPy array suitable for uPlot.
12
+
13
+ Args:
14
+ array: NumPy array to convert to JavaScript.
15
+
16
+ Returns:
17
+ String representation of the array.
18
+ """
19
+ array_str = np.array2string(
20
+ array,
21
+ precision=64,
22
+ separator=",",
23
+ threshold=np.inf,
24
+ )
25
+ return array_str.replace("nan", "null")
26
+
27
+
28
+ def js(code: str) -> str:
29
+ """Wrap a code string so that it is processed as output JavaScript.
30
+
31
+ Args:
32
+ code: Code string to wrap.
33
+
34
+ Returns:
35
+ Wrapped string.
36
+ """
37
+ return f"<script>{code}</script>"
@@ -11,14 +11,14 @@ import tempfile
11
11
  from datetime import datetime
12
12
 
13
13
 
14
- def write_tmpfile(html: str) -> str:
15
- """Write output page.
14
+ def write_html_tempfile(html: str) -> str:
15
+ """Write string to an HTML file.
16
16
 
17
17
  Args:
18
18
  html: HTML content.
19
19
 
20
20
  Returns:
21
- Name of the output file (a temporary file).
21
+ Name of the output temporary file.
22
22
  """
23
23
  filename: str = ""
24
24
  with tempfile.NamedTemporaryFile(
@@ -1 +0,0 @@
1
- include uplot/uplot/uPlot.*
@@ -1,3 +0,0 @@
1
- # uplot-python
2
-
3
- Python wrapper for [μPlot](https://github.com/leeoniya/uPlot) 📈
@@ -1,12 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- #
4
- # SPDX-License-Identifier: Apache-2.0
5
- # Copyright 2024 Inria
6
-
7
- import numpy as np
8
- import uplot
9
-
10
- if __name__ == "__main__":
11
- t = np.arange(10)
12
- uplot.plot(t, np.exp(0.42 * t))
@@ -1,228 +0,0 @@
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
@@ -1,36 +0,0 @@
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)
File without changes
File without changes