youplot 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- youplot/__init__.py +37 -0
- youplot/colors/__init__.py +0 -0
- youplot/colors/palette.py +106 -0
- youplot/examples/basic_line.py +123 -0
- youplot/figure.py +518 -0
- youplot/options/__init__.py +0 -0
- youplot/options/annotations.py +60 -0
- youplot/options/axes.py +22 -0
- youplot/render/css.py +578 -0
- youplot/render/html.py +320 -0
- youplot/render/js.py +1080 -0
- youplot/render/scatter_js.py +195 -0
- youplot/render/serializer.py +242 -0
- youplot/series/__init__.py +2 -0
- youplot/series/line.py +70 -0
- youplot/series/scatter.py +76 -0
- youplot/themes/__init__.py +0 -0
- youplot/themes/base.py +105 -0
- youplot/utils/__init__.py +0 -0
- youplot/utils/browser.py +22 -0
- youplot/utils/data.py +150 -0
- youplot/vendor/__init__.py +12 -0
- youplot/vendor/__pycache__/__init__.cpython-311.pyc +0 -0
- youplot/vendor/uplot.iife.min.js +2 -0
- youplot/vendor/uplot.min.css +1 -0
- youplot-1.0.0.dist-info/METADATA +224 -0
- youplot-1.0.0.dist-info/RECORD +30 -0
- youplot-1.0.0.dist-info/WHEEL +5 -0
- youplot-1.0.0.dist-info/licenses/LICENSE +21 -0
- youplot-1.0.0.dist-info/top_level.txt +1 -0
youplot/__init__.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
youplot — Extremely fast, lightweight timeseries charts for Python.
|
|
3
|
+
Powered by uPlot (https://github.com/leeoniya/uPlot).
|
|
4
|
+
|
|
5
|
+
Quick start::
|
|
6
|
+
|
|
7
|
+
import youplot as fp
|
|
8
|
+
|
|
9
|
+
fig1 = fp.Figure(title="Temperature", zoom=True)
|
|
10
|
+
fig1.line(ts_ms, temp, label="°C", color="#f97316", fill=True)
|
|
11
|
+
fig1.band(y_lo=18, y_hi=24, label="Comfort zone", color="#10b981")
|
|
12
|
+
fig1.tag(x_start=ts_ms[0], x_end=ts_ms[3600], label="Night")
|
|
13
|
+
fig1.pin(ts_ms[peak], label="Peak: 38°C", y_frac=0.1)
|
|
14
|
+
|
|
15
|
+
fig2 = fp.Figure(title="Humidity", zoom=True)
|
|
16
|
+
fig2.line(ts_ms, humidity, label="%", color="#6366f1")
|
|
17
|
+
|
|
18
|
+
# Combine → synced crosshair, one HTML file
|
|
19
|
+
dash = fig1 + fig2 # or fp.combine(fig1, fig2)
|
|
20
|
+
dash.save("dashboard.html")
|
|
21
|
+
dash.show()
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from youplot.figure import Figure, Dashboard, combine
|
|
25
|
+
from youplot.options.annotations import VLine, HLine, Region, Band, Pin
|
|
26
|
+
from youplot.colors.palette import resolve as resolve_color, NAMED as COLORS
|
|
27
|
+
from youplot.themes.base import LIGHT, DARK
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"Figure", "Dashboard", "combine",
|
|
31
|
+
"VLine", "HLine", "Region", "Band", "Pin",
|
|
32
|
+
"resolve_color", "COLORS", "LIGHT", "DARK",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
__version__ = "1.0.0"
|
|
36
|
+
__author__ = "youplot contributors"
|
|
37
|
+
__license__ = "MIT"
|
|
File without changes
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Color palette for youplot.
|
|
3
|
+
Named colors resolve to hex. Default cycle ensures series never clash.
|
|
4
|
+
Inspired by Tailwind + Observable's categorical palette.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# ── Named color map ────────────────────────────────────────────────────────────
|
|
8
|
+
# Tailwind-derived but tuned for data viz (slightly more saturated)
|
|
9
|
+
|
|
10
|
+
NAMED = {
|
|
11
|
+
# Blues / purples
|
|
12
|
+
"indigo": "#6366f1",
|
|
13
|
+
"violet": "#7c3aed",
|
|
14
|
+
"purple": "#9333ea",
|
|
15
|
+
"blue": "#3b82f6",
|
|
16
|
+
"sky": "#0ea5e9",
|
|
17
|
+
"cyan": "#06b6d4",
|
|
18
|
+
|
|
19
|
+
# Greens
|
|
20
|
+
"emerald": "#10b981",
|
|
21
|
+
"green": "#22c55e",
|
|
22
|
+
"teal": "#14b8a6",
|
|
23
|
+
|
|
24
|
+
# Warm
|
|
25
|
+
"amber": "#f59e0b",
|
|
26
|
+
"orange": "#f97316",
|
|
27
|
+
"yellow": "#eab308",
|
|
28
|
+
|
|
29
|
+
# Reds / pinks
|
|
30
|
+
"rose": "#f43f5e",
|
|
31
|
+
"red": "#ef4444",
|
|
32
|
+
"pink": "#ec4899",
|
|
33
|
+
|
|
34
|
+
# Neutrals
|
|
35
|
+
"slate": "#64748b",
|
|
36
|
+
"gray": "#6b7280",
|
|
37
|
+
"zinc": "#71717a",
|
|
38
|
+
"white": "#ffffff",
|
|
39
|
+
"black": "#000000",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# ── Default series cycle ────────────────────────────────────────────────────────
|
|
43
|
+
# Ordered for max visual separation. Observable-inspired but warmer.
|
|
44
|
+
|
|
45
|
+
DEFAULT_CYCLE = [
|
|
46
|
+
"#6366f1", # indigo
|
|
47
|
+
"#f59e0b", # amber
|
|
48
|
+
"#10b981", # emerald
|
|
49
|
+
"#f43f5e", # rose
|
|
50
|
+
"#0ea5e9", # sky
|
|
51
|
+
"#9333ea", # purple
|
|
52
|
+
"#f97316", # orange
|
|
53
|
+
"#14b8a6", # teal
|
|
54
|
+
"#ec4899", # pink
|
|
55
|
+
"#3b82f6", # blue
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
# Dark theme variants — slightly lighter/more saturated for dark bg legibility
|
|
59
|
+
DEFAULT_CYCLE_DARK = [
|
|
60
|
+
"#818cf8", # indigo-400
|
|
61
|
+
"#fbbf24", # amber-400
|
|
62
|
+
"#34d399", # emerald-400
|
|
63
|
+
"#fb7185", # rose-400
|
|
64
|
+
"#38bdf8", # sky-400
|
|
65
|
+
"#c084fc", # purple-400
|
|
66
|
+
"#fb923c", # orange-400
|
|
67
|
+
"#2dd4bf", # teal-400
|
|
68
|
+
"#f472b6", # pink-400
|
|
69
|
+
"#60a5fa", # blue-400
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def resolve(color: str | None, dark: bool = False) -> str:
|
|
74
|
+
"""
|
|
75
|
+
Resolve a color name or passthrough a hex/rgb string.
|
|
76
|
+
|
|
77
|
+
resolve("indigo") → "#6366f1"
|
|
78
|
+
resolve("#ff0000") → "#ff0000"
|
|
79
|
+
resolve("rgb(0,0,0)") → "rgb(0,0,0)"
|
|
80
|
+
resolve(None) → raises ValueError
|
|
81
|
+
"""
|
|
82
|
+
if color is None:
|
|
83
|
+
raise ValueError("Color cannot be None — use next_color() to get default")
|
|
84
|
+
if color in NAMED:
|
|
85
|
+
return NAMED[color]
|
|
86
|
+
if color.startswith("#") or color.startswith("rgb"):
|
|
87
|
+
return color
|
|
88
|
+
raise ValueError(
|
|
89
|
+
f"Unknown color '{color}'. Use a named color ({', '.join(NAMED)}) or a hex string."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class ColorCycle:
|
|
94
|
+
"""Stateful iterator over the default color cycle. One per Figure."""
|
|
95
|
+
|
|
96
|
+
def __init__(self, dark: bool = False):
|
|
97
|
+
self._cycle = DEFAULT_CYCLE_DARK if dark else DEFAULT_CYCLE
|
|
98
|
+
self._idx = 0
|
|
99
|
+
|
|
100
|
+
def next(self) -> str:
|
|
101
|
+
color = self._cycle[self._idx % len(self._cycle)]
|
|
102
|
+
self._idx += 1
|
|
103
|
+
return color
|
|
104
|
+
|
|
105
|
+
def reset(self):
|
|
106
|
+
self._idx = 0
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Basic example — weather station telemetry over a 24-hour period.
|
|
3
|
+
Two synced charts via up.combine() / the + operator.
|
|
4
|
+
|
|
5
|
+
Run from the youplot parent directory:
|
|
6
|
+
python -m youplot.examples.basic_line
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sys, os
|
|
10
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
|
|
11
|
+
|
|
12
|
+
import math, time
|
|
13
|
+
import youplot as up
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def make_data(n=1440, seed=7):
|
|
17
|
+
base_ts = int(time.time()) - 86400
|
|
18
|
+
ts_ms = [(base_ts + i * 60) * 1000 for i in range(n)]
|
|
19
|
+
|
|
20
|
+
def noise(scale, offset=0):
|
|
21
|
+
import random; random.seed(seed + offset)
|
|
22
|
+
return [random.gauss(0, scale) for _ in range(n)]
|
|
23
|
+
|
|
24
|
+
def diurnal(lo, hi, phase=0):
|
|
25
|
+
return [lo + (hi-lo)*0.5*(1-math.cos(2*math.pi*(i/n+phase))) for i in range(n)]
|
|
26
|
+
|
|
27
|
+
temp_c = [round(diurnal(14,31)[i] + noise(0.3,1)[i], 1) for i in range(n)]
|
|
28
|
+
feels = [round(temp_c[i] - 2.5 + 0.4*math.sin(i/120) + noise(1.5,2)[i], 1) for i in range(n)]
|
|
29
|
+
humidity = [round(max(10, min(100, diurnal(80,35,0.5)[i] + noise(2,3)[i])), 1) for i in range(n)]
|
|
30
|
+
dew_point = [round(temp_c[i] - (100-humidity[i])/5 + noise(0.6,4)[i], 1) for i in range(n)]
|
|
31
|
+
pressure = [round(1012 + 6*math.sin(2*math.pi*i/n) + 3*math.cos(2*math.pi*i/(n*0.4)) + noise(0.4,5)[i], 1) for i in range(n)]
|
|
32
|
+
wind_spd = [round(max(0, 8 + 6*math.sin(i/180+1) + abs(noise(2,6)[i])), 1) for i in range(n)]
|
|
33
|
+
|
|
34
|
+
day_frac = lambda i: (i%n)/n
|
|
35
|
+
solar = [max(0, round(900*math.sin(math.pi*max(0,(day_frac(i)-0.25))/0.5)**1.2 + noise(15,7)[i], 0))
|
|
36
|
+
if 0.25 <= day_frac(i) <= 0.75 else 0 for i in range(n)]
|
|
37
|
+
uv = [round(max(0, min(11, solar[i]/80 + noise(0.2,8)[i])), 1) for i in range(n)]
|
|
38
|
+
pm25 = [round(max(0, 12
|
|
39
|
+
+ 20*math.exp(-((i-n*0.30)**2)/(2*(n*0.04)**2))
|
|
40
|
+
+ 15*math.exp(-((i-n*0.75)**2)/(2*(n*0.04)**2))
|
|
41
|
+
+ abs(noise(3,9)[i])), 1) for i in range(n)]
|
|
42
|
+
rain = [0.0]*n
|
|
43
|
+
sc = int(n*0.58)
|
|
44
|
+
for i in range(n):
|
|
45
|
+
d = i - sc
|
|
46
|
+
if abs(d) < 60:
|
|
47
|
+
rain[i] = round(max(0, 8*math.exp(-(d**2)/800) + noise(0.3,10)[i]), 2)
|
|
48
|
+
|
|
49
|
+
return ts_ms, temp_c, feels, humidity, dew_point, pressure, wind_spd, solar, uv, pm25, rain
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def main():
|
|
53
|
+
ts_ms, temp_c, feels, humidity, dew_point, pressure, wind_spd, solar, uv, pm25, rain = make_data()
|
|
54
|
+
n = len(ts_ms)
|
|
55
|
+
|
|
56
|
+
# ── Chart 1: Temperature, Humidity, Wind, Pressure ───────────────────────
|
|
57
|
+
fig1 = up.Figure(
|
|
58
|
+
title="Temperature · Humidity · Wind · Pressure",
|
|
59
|
+
subtitle="Hover to sync crosshair · drag to zoom · vertical drag zooms Y · annotate to pin notes",
|
|
60
|
+
theme="light", height=300,
|
|
61
|
+
y_label="Temp °C / Humidity % / Wind km·h⁻¹",
|
|
62
|
+
y_right_label="Pressure hPa",
|
|
63
|
+
zoom=True, legend=True,
|
|
64
|
+
)
|
|
65
|
+
fig1.line(ts_ms, temp_c, label="Temp °C", color="#f97316", width=2.5, hover_unit=" °C")
|
|
66
|
+
fig1.line(ts_ms, feels, label="Feels Like °C", color="#fb923c", width=1.5, dash=True, hover_unit=" °C")
|
|
67
|
+
fig1.line(ts_ms, humidity, label="Humidity %", color="#38bdf8", width=2.0, fill=True, fill_opacity=0.07, hover_unit="%")
|
|
68
|
+
fig1.line(ts_ms, dew_point, label="Dew Point °C", color="#0ea5e9", width=1.5, dash=True, hover_unit=" °C")
|
|
69
|
+
fig1.line(ts_ms, wind_spd, label="Wind km/h", color="#a3e635", width=1.5, hover_unit=" km/h")
|
|
70
|
+
# fig1.line(ts_ms, pressure, label="Pressure hPa", color="#8b5cf6", width=1.5, axis="right", hover_unit=" hPa")
|
|
71
|
+
|
|
72
|
+
fig1.band(y_lo=25, y_hi=35, label="Heat stress", color="#f97316", opacity=0.07)
|
|
73
|
+
fig1.band(y_lo=70, y_hi=100, label="High humidity", color="#38bdf8", opacity=0.06)
|
|
74
|
+
|
|
75
|
+
fig1.region(x_start=ts_ms[0], x_end=ts_ms[int(n*0.25)], color="#6366f1", opacity=0.04)
|
|
76
|
+
fig1.region(x_start=ts_ms[int(n*0.88)], x_end=ts_ms[-1], color="#6366f1", opacity=0.04)
|
|
77
|
+
|
|
78
|
+
fig1.vline(x=ts_ms[int(n*0.25)], label="Sunrise", color="#f97316")
|
|
79
|
+
fig1.vline(x=ts_ms[int(n*0.75)], label="Sunset", color="#8b5cf6")
|
|
80
|
+
fig1.hline(y=25, label="Heat threshold", color="#f97316", dash=True)
|
|
81
|
+
fig1.hline(y=60, label="High humidity", color="#38bdf8", dash=True)
|
|
82
|
+
|
|
83
|
+
fig1.tag(x_start=ts_ms[int(n*0.28)], x_end=ts_ms[int(n*0.40)],
|
|
84
|
+
label="Morning Rush", color="#f43f5e", removable=False)
|
|
85
|
+
fig1.tag(x_start=ts_ms[int(n*0.72)], x_end=ts_ms[int(n*0.80)],
|
|
86
|
+
label="Evening Peak", color="#8b5cf6", removable=True)
|
|
87
|
+
|
|
88
|
+
# Code-defined annotation pins
|
|
89
|
+
# fig1.pin(ts_ms[int(n*0.25)], label="Sunrise crossover", y_frac=0.15, color="#f97316")
|
|
90
|
+
# fig1.pin(ts_ms[int(n*0.52)], label="Temp peak 31°C", y_frac=0.05, color="#f43f5e")
|
|
91
|
+
|
|
92
|
+
# ── Chart 2: Solar, Air Quality, Rain ────────────────────────────────────
|
|
93
|
+
fig2 = up.Figure(
|
|
94
|
+
title="Solar · Air Quality · Rain",
|
|
95
|
+
subtitle="Crosshair synced with chart above",
|
|
96
|
+
theme="light", height=300,
|
|
97
|
+
y_label="Solar W/m² / UV / PM2.5 µg/m³",
|
|
98
|
+
y_right_label="Rain mm/h",
|
|
99
|
+
zoom=True, legend=True,
|
|
100
|
+
)
|
|
101
|
+
fig2.line(ts_ms, solar, label="Solar W/m²", color="#facc15", width=2.0, fill=True, fill_opacity=0.08, hover_unit=" W/m²")
|
|
102
|
+
fig2.line(ts_ms, uv, label="UV Index", color="#fbbf24", width=1.5, dash=True)
|
|
103
|
+
fig2.line(ts_ms, pm25, label="PM2.5 µg/m³", color="#f43f5e", width=1.5, hover_unit=" µg/m³")
|
|
104
|
+
fig2.line(ts_ms, rain, label="Rain mm/h", color="#06b6d4", width=1.5, fill=True, fill_opacity=0.15,
|
|
105
|
+
axis="right", hover_unit=" mm/h")
|
|
106
|
+
|
|
107
|
+
fig2.band(y_lo=35, y_hi=150, label="PM2.5 Unhealthy", color="#f43f5e", opacity=0.05)
|
|
108
|
+
fig2.vline(x=ts_ms[int(n*0.58)], label="Peak Rain", color="#06b6d4", dash=True)
|
|
109
|
+
fig2.hline(y=35, label="PM2.5 Moderate", color="#f43f5e", dash=True)
|
|
110
|
+
fig2.tag(x_start=ts_ms[int(n*0.55)], x_end=ts_ms[int(n*0.62)],
|
|
111
|
+
label="Afternoon Shower", color="#06b6d4", removable=False)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
dash = up.combine(fig1, fig2, title="Weather Station — 24h Overview")
|
|
116
|
+
|
|
117
|
+
out = dash.save("/tmp/youplot_weather_demo.html")
|
|
118
|
+
print(f"✓ Saved to {out}")
|
|
119
|
+
dash.show()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
if __name__ == "__main__":
|
|
123
|
+
main()
|