pyvmote 0.1.5__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.
- pyvmote/Graph_generator.py +210 -0
- pyvmote/Service.py +165 -0
- pyvmote/__init__.py +8 -0
- pyvmote/pyvmote.py +70 -0
- pyvmote-0.1.5.dist-info/METADATA +51 -0
- pyvmote-0.1.5.dist-info/RECORD +8 -0
- pyvmote-0.1.5.dist-info/WHEEL +5 -0
- pyvmote-0.1.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import matplotlib.pyplot as plt
|
|
2
|
+
import mpld3
|
|
3
|
+
from mpld3 import plugins
|
|
4
|
+
import os
|
|
5
|
+
import json
|
|
6
|
+
from scipy.stats import gaussian_kde
|
|
7
|
+
import numpy as np
|
|
8
|
+
import re
|
|
9
|
+
import shutil
|
|
10
|
+
import warnings
|
|
11
|
+
|
|
12
|
+
class Graph:
|
|
13
|
+
def __init__(self):
|
|
14
|
+
warnings.filterwarnings("ignore", category=UserWarning)
|
|
15
|
+
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
|
16
|
+
|
|
17
|
+
self.path = os.path.dirname(os.path.abspath(__file__))
|
|
18
|
+
self.n_plot = 0
|
|
19
|
+
self.history_file = os.path.join(self.path, "static", "graph_history.json")
|
|
20
|
+
|
|
21
|
+
if not os.path.exists(self.history_file):
|
|
22
|
+
with open(self.history_file, "w") as f:
|
|
23
|
+
json.dump([], f)
|
|
24
|
+
|
|
25
|
+
def clear_history(self):
|
|
26
|
+
if os.path.exists(self.history_file):
|
|
27
|
+
os.remove(self.history_file)
|
|
28
|
+
with open(self.history_file, "w") as f:
|
|
29
|
+
json.dump([], f)
|
|
30
|
+
|
|
31
|
+
def save_graph_to_history(self, graph_name, graph_type, title):
|
|
32
|
+
with open(self.history_file, "r") as f:
|
|
33
|
+
history = json.load(f)
|
|
34
|
+
|
|
35
|
+
# Verifica si ya existe un gráfico con ese título
|
|
36
|
+
for graph in history:
|
|
37
|
+
if graph["title"] == title:
|
|
38
|
+
raise ValueError(f"Ya existe un gráfico con el título '{title}'. Usa uno diferente.")
|
|
39
|
+
|
|
40
|
+
history.append({"name": graph_name, "type": graph_type, "title": title})
|
|
41
|
+
|
|
42
|
+
with open(self.history_file, "w") as f:
|
|
43
|
+
json.dump(history, f)
|
|
44
|
+
|
|
45
|
+
def _sanitize_title(self, title):
|
|
46
|
+
return re.sub(r'[^a-zA-Z0-9_-]', '_', title.replace(' ', '_'))
|
|
47
|
+
|
|
48
|
+
def _prepare_paths(self, title):
|
|
49
|
+
images_dir = os.path.join(self.path, "static", "images")
|
|
50
|
+
html_dir = os.path.join(self.path, "static", "html")
|
|
51
|
+
os.makedirs(images_dir, exist_ok=True)
|
|
52
|
+
os.makedirs(html_dir, exist_ok=True)
|
|
53
|
+
|
|
54
|
+
safe_title = self._sanitize_title(title)
|
|
55
|
+
image_name = f"{safe_title}.png"
|
|
56
|
+
html_name = f"{safe_title}.html"
|
|
57
|
+
|
|
58
|
+
return images_dir, html_dir, image_name, html_name
|
|
59
|
+
|
|
60
|
+
def _finalize_plot(self, fig, image_path, html_path, labels, scatter, plot_type, interactive, title):
|
|
61
|
+
if interactive:
|
|
62
|
+
if labels and plot_type in ["line", "scatter", "bar", "density"]:
|
|
63
|
+
if plot_type == "bar":
|
|
64
|
+
for rect, label in zip(scatter, labels):
|
|
65
|
+
plugins.connect(fig, plugins.LineLabelTooltip(rect, label))
|
|
66
|
+
else:
|
|
67
|
+
plugins.connect(fig, plugins.PointLabelTooltip(scatter[0], labels=labels))
|
|
68
|
+
|
|
69
|
+
mpld3.save_html(fig, html_path)
|
|
70
|
+
self.save_graph_to_history(os.path.basename(html_path), "html", title)
|
|
71
|
+
plt.close()
|
|
72
|
+
return os.path.basename(html_path)
|
|
73
|
+
else:
|
|
74
|
+
plt.savefig(image_path, dpi=300, bbox_inches='tight')
|
|
75
|
+
self.save_graph_to_history(os.path.basename(image_path), "image", title)
|
|
76
|
+
plt.close()
|
|
77
|
+
return os.path.basename(image_path)
|
|
78
|
+
|
|
79
|
+
def save_as_format(self, fig, title, extension="png"):
|
|
80
|
+
valid_exts = ["png", "jpg", "jpeg", "svg", "pdf"]
|
|
81
|
+
if extension not in valid_exts:
|
|
82
|
+
raise ValueError(f"Formato no soportado: {extension}")
|
|
83
|
+
|
|
84
|
+
output_dir = os.path.join(self.path, "static", "images")
|
|
85
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
86
|
+
safe_title = self._sanitize_title(title)
|
|
87
|
+
output_path = os.path.join(output_dir, f"{safe_title}.{extension}")
|
|
88
|
+
fig.savefig(output_path, dpi=300, bbox_inches='tight')
|
|
89
|
+
return output_path
|
|
90
|
+
|
|
91
|
+
def line_plot(self, x, y, xname="X", yname="Y", title="Line Graph", interactive=True, color='blue', linewidth=2, xlim=None, ylim=None):
|
|
92
|
+
fig, ax = plt.subplots(figsize=(12, 7))
|
|
93
|
+
scatter = ax.plot(x, y, marker='o', linestyle='-', color=color, linewidth=linewidth)
|
|
94
|
+
labels = [f"({xi}, {yi})" for xi, yi in zip(x, y)]
|
|
95
|
+
ax.set_xlabel(xname)
|
|
96
|
+
ax.set_ylabel(yname)
|
|
97
|
+
ax.set_title(title)
|
|
98
|
+
if xlim: ax.set_xlim(xlim)
|
|
99
|
+
if ylim: ax.set_ylim(ylim)
|
|
100
|
+
images_dir, html_dir, image_name, html_name = self._prepare_paths(title)
|
|
101
|
+
return self._finalize_plot(fig, os.path.join(images_dir, image_name), os.path.join(html_dir, html_name), labels, scatter, "line", interactive, title)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def scatter_plot(self, x, y, xname="X", yname="Y", title="Scatter Plot", interactive=True, color='blue', xlim=None, ylim=None):
|
|
105
|
+
fig, ax = plt.subplots(figsize=(12, 7))
|
|
106
|
+
scatter = ax.scatter(x, y, color=color)
|
|
107
|
+
labels = [f"({xi}, {yi})" for xi, yi in zip(x, y)]
|
|
108
|
+
ax.set_xlabel(xname)
|
|
109
|
+
ax.set_ylabel(yname)
|
|
110
|
+
ax.set_title(title)
|
|
111
|
+
if xlim: ax.set_xlim(xlim)
|
|
112
|
+
if ylim: ax.set_ylim(ylim)
|
|
113
|
+
images_dir, html_dir, image_name, html_name = self._prepare_paths(title)
|
|
114
|
+
return self._finalize_plot(fig, os.path.join(images_dir, image_name), os.path.join(html_dir, html_name), labels, [scatter], "scatter", interactive, title)
|
|
115
|
+
|
|
116
|
+
def bar_plot(self, x, y, xname="X", yname="Y", title="Bar Plot", interactive=True, color='blue', xlim=None, ylim=None):
|
|
117
|
+
fig, ax = plt.subplots(figsize=(12, 7))
|
|
118
|
+
scatter = ax.bar(x, y, color=color)
|
|
119
|
+
labels = [f"({xi}, {yi})" for xi, yi in zip(x, y)]
|
|
120
|
+
ax.set_xlabel(xname)
|
|
121
|
+
ax.set_ylabel(yname)
|
|
122
|
+
ax.set_title(title)
|
|
123
|
+
if xlim: ax.set_xlim(xlim)
|
|
124
|
+
if ylim: ax.set_ylim(ylim)
|
|
125
|
+
images_dir, html_dir, image_name, html_name = self._prepare_paths(title)
|
|
126
|
+
return self._finalize_plot(fig, os.path.join(images_dir, image_name), os.path.join(html_dir, html_name), labels, scatter, "bar", interactive, title)
|
|
127
|
+
|
|
128
|
+
def hist_plot(self, x, xname="Value", yname="Frequency", title="Histogram", bins=20, interactive=True, color='blue', xlim=None, ylim=None):
|
|
129
|
+
fig, ax = plt.subplots(figsize=(12, 7))
|
|
130
|
+
scatter = ax.hist(x, bins=bins, edgecolor='black', color=color)
|
|
131
|
+
ax.set_xlabel(xname)
|
|
132
|
+
ax.set_ylabel(yname)
|
|
133
|
+
ax.set_title(title)
|
|
134
|
+
if xlim: ax.set_xlim(xlim)
|
|
135
|
+
if ylim: ax.set_ylim(ylim)
|
|
136
|
+
images_dir, html_dir, image_name, html_name = self._prepare_paths(title)
|
|
137
|
+
return self._finalize_plot(fig, os.path.join(images_dir, image_name), os.path.join(html_dir, html_name), [], [], "hist", interactive, title)
|
|
138
|
+
|
|
139
|
+
def box_plot(self, x, xname="", yname="Value", title="Box Plot", interactive=True):
|
|
140
|
+
fig, ax = plt.subplots(figsize=(12, 7))
|
|
141
|
+
scatter = ax.boxplot(x)
|
|
142
|
+
ax.set_ylabel(yname)
|
|
143
|
+
ax.set_xlabel(xname)
|
|
144
|
+
ax.set_title(title)
|
|
145
|
+
images_dir, html_dir, image_name, html_name = self._prepare_paths(title)
|
|
146
|
+
return self._finalize_plot(fig, os.path.join(images_dir, image_name), os.path.join(html_dir, html_name), [], [], "box", interactive, title)
|
|
147
|
+
|
|
148
|
+
def density_plot(self, x, xname="X", yname="Density", title="Density Plot", interactive=True, color='blue', xlim=None, ylim=None):
|
|
149
|
+
fig, ax = plt.subplots(figsize=(12, 7))
|
|
150
|
+
kde = gaussian_kde(x)
|
|
151
|
+
x_vals = np.linspace(min(x), max(x), 200)
|
|
152
|
+
y_vals = kde(x_vals)
|
|
153
|
+
scatter = ax.plot(x_vals, y_vals, color=color)
|
|
154
|
+
labels = [f"({xi:.2f}, {yi:.2f})" for xi, yi in zip(x_vals, y_vals)]
|
|
155
|
+
ax.set_xlabel(xname)
|
|
156
|
+
ax.set_ylabel(yname)
|
|
157
|
+
ax.set_title(title)
|
|
158
|
+
if xlim: ax.set_xlim(xlim)
|
|
159
|
+
if ylim: ax.set_ylim(ylim)
|
|
160
|
+
images_dir, html_dir, image_name, html_name = self._prepare_paths(title)
|
|
161
|
+
return self._finalize_plot(fig, os.path.join(images_dir, image_name), os.path.join(html_dir, html_name), labels, scatter, "density", interactive, title)
|
|
162
|
+
|
|
163
|
+
def pie_plot(self, sizes, labels=None, title="Pie Chart", interactive=True, colors=None):
|
|
164
|
+
fig, ax = plt.subplots(figsize=(8, 8))
|
|
165
|
+
ax.pie(sizes, labels=labels, autopct='%1.1f%%', startangle=90, colors=colors)
|
|
166
|
+
ax.set_title(title)
|
|
167
|
+
images_dir, html_dir, image_name, html_name = self._prepare_paths(title)
|
|
168
|
+
return self._finalize_plot(fig, os.path.join(images_dir, image_name), os.path.join(html_dir, html_name), [], [], "pie", interactive, title)
|
|
169
|
+
|
|
170
|
+
def cluster_plot(self, data, labels, title="Cluster Plot", interactive=True, cmap='viridis', xlim=None, ylim=None):
|
|
171
|
+
fig, ax = plt.subplots(figsize=(12, 7))
|
|
172
|
+
scatter = ax.scatter(data[:, 0], data[:, 1], c=labels, cmap=cmap)
|
|
173
|
+
ax.set_title(title)
|
|
174
|
+
ax.set_xlabel("X")
|
|
175
|
+
ax.set_ylabel("Y")
|
|
176
|
+
if xlim: ax.set_xlim(xlim)
|
|
177
|
+
if ylim: ax.set_ylim(ylim)
|
|
178
|
+
images_dir, html_dir, image_name, html_name = self._prepare_paths(title)
|
|
179
|
+
return self._finalize_plot(fig, os.path.join(images_dir, image_name), os.path.join(html_dir, html_name), [], [scatter], "cluster", interactive, title)
|
|
180
|
+
|
|
181
|
+
# 4. Personalización en otros gráficos
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def save_as_format(self, title, extension="png", target_folder="exports"):
|
|
185
|
+
|
|
186
|
+
valid_exts = ["png", "jpg", "jpeg", "svg", "pdf"]
|
|
187
|
+
if extension not in valid_exts:
|
|
188
|
+
raise ValueError(f"Formato no soportado: {extension}")
|
|
189
|
+
|
|
190
|
+
safe_title = self._sanitize_title(title)
|
|
191
|
+
original_path = os.path.join(self.path, "static", "images", f"{safe_title}.png")
|
|
192
|
+
|
|
193
|
+
if not os.path.exists(original_path):
|
|
194
|
+
raise FileNotFoundError(f"No se encontró el gráfico original: {original_path}")
|
|
195
|
+
|
|
196
|
+
# Crear carpeta destino si no existe
|
|
197
|
+
os.makedirs(target_folder, exist_ok=True)
|
|
198
|
+
output_path = os.path.join(target_folder, f"{safe_title}.{extension}")
|
|
199
|
+
|
|
200
|
+
# Si el formato es el mismo, simplemente copiar
|
|
201
|
+
if extension == "png":
|
|
202
|
+
shutil.copyfile(original_path, output_path)
|
|
203
|
+
else:
|
|
204
|
+
# Leer imagen y volver a guardarla en nuevo formato
|
|
205
|
+
from PIL import Image
|
|
206
|
+
with Image.open(original_path) as img:
|
|
207
|
+
format_map = {"jpg": "JPEG", "jpeg": "JPEG", "png": "PNG", "pdf": "PDF", "svg": "SVG"}
|
|
208
|
+
img.convert("RGB").save(output_path, format_map[extension.lower()])
|
|
209
|
+
|
|
210
|
+
return output_path
|
pyvmote/Service.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
from fastapi import FastAPI, WebSocketDisconnect, WebSocket, Request
|
|
2
|
+
from fastapi.responses import JSONResponse
|
|
3
|
+
from fastapi.staticfiles import StaticFiles
|
|
4
|
+
from fastapi.templating import Jinja2Templates
|
|
5
|
+
import uvicorn
|
|
6
|
+
import os
|
|
7
|
+
import asyncio
|
|
8
|
+
import threading
|
|
9
|
+
import shutil
|
|
10
|
+
import json
|
|
11
|
+
|
|
12
|
+
class Server:
|
|
13
|
+
def __init__(self):
|
|
14
|
+
self.port = ""
|
|
15
|
+
self.app = FastAPI()
|
|
16
|
+
self.ws_connection = None
|
|
17
|
+
self.last_timestamp = 0
|
|
18
|
+
self.loop = None
|
|
19
|
+
self.start = False
|
|
20
|
+
self.init_server()
|
|
21
|
+
|
|
22
|
+
def init_server(self):
|
|
23
|
+
self.base_path = os.path.dirname(os.path.abspath(__file__))
|
|
24
|
+
self.image_folder = os.path.join(self.base_path, "static", "images")
|
|
25
|
+
self.html_folder = os.path.join(self.base_path, "static", "html")
|
|
26
|
+
self.templates_path = os.path.join(self.base_path, "templates")
|
|
27
|
+
|
|
28
|
+
self.app.mount("/static", StaticFiles(directory=os.path.join(self.base_path, "static")), name="static")
|
|
29
|
+
|
|
30
|
+
self.templates = Jinja2Templates(directory=self.templates_path)
|
|
31
|
+
|
|
32
|
+
self.app.get("/")(self.index)
|
|
33
|
+
self.app.add_api_route("/preview", self.preview_page)
|
|
34
|
+
self.app.get("/latest")(self.latest_image)
|
|
35
|
+
self.app.add_api_route("/rename", self.rename_graph, methods=["POST"])
|
|
36
|
+
|
|
37
|
+
self.app.on_event("startup")(self.startup_event)
|
|
38
|
+
self.app.websocket("/ws")(self.websocket_endpoint)
|
|
39
|
+
|
|
40
|
+
async def startup_event(self):
|
|
41
|
+
self.loop = asyncio.get_running_loop()
|
|
42
|
+
|
|
43
|
+
async def websocket_endpoint(self, websocket: WebSocket):
|
|
44
|
+
await websocket.accept()
|
|
45
|
+
self.ws_connection = websocket
|
|
46
|
+
try:
|
|
47
|
+
while True:
|
|
48
|
+
await websocket.receive_text()
|
|
49
|
+
except WebSocketDisconnect:
|
|
50
|
+
self.ws_connection = None
|
|
51
|
+
|
|
52
|
+
def get_image_files(self):
|
|
53
|
+
valid_extensions = ('.png', '.jpg', '.jpeg', '.gif', '.bmp')
|
|
54
|
+
if not os.path.exists(self.image_folder):
|
|
55
|
+
return []
|
|
56
|
+
return [f for f in os.listdir(self.image_folder) if f.lower().endswith(valid_extensions)]
|
|
57
|
+
|
|
58
|
+
def get_html_files(self):
|
|
59
|
+
if not os.path.exists(self.html_folder):
|
|
60
|
+
return []
|
|
61
|
+
return [f for f in os.listdir(self.html_folder) if f.endswith(".html")]
|
|
62
|
+
|
|
63
|
+
def get_latest_graphs(self):
|
|
64
|
+
images = self.get_image_files()
|
|
65
|
+
htmls = self.get_html_files()
|
|
66
|
+
return images, htmls
|
|
67
|
+
|
|
68
|
+
async def index(self, request: Request):
|
|
69
|
+
latest_img, latest_html = self.get_latest_graphs()
|
|
70
|
+
return self.templates.TemplateResponse("index.html", {"request": request, "latest_img": latest_img[0] if latest_img else None, "latest_html": latest_html[0] if latest_html else None})
|
|
71
|
+
|
|
72
|
+
async def preview_page(self,request: Request):
|
|
73
|
+
graphs = self.get_ordered_graphs()
|
|
74
|
+
return self.templates.TemplateResponse("preview.html", {"request": request, "graphs": graphs})
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def show_port(self):
|
|
78
|
+
print(f"Servidor corriendo en http://localhost:{self.port}")
|
|
79
|
+
|
|
80
|
+
def start_server(self, port):
|
|
81
|
+
self.port = port
|
|
82
|
+
self.start = True
|
|
83
|
+
config = uvicorn.Config(self.app, host="0.0.0.0", port=self.port, log_level="warning")
|
|
84
|
+
self.server = uvicorn.Server(config)
|
|
85
|
+
self.server_thread = threading.Thread(target=self.server.run, daemon=True)
|
|
86
|
+
self.server_thread.start()
|
|
87
|
+
self.show_port()
|
|
88
|
+
|
|
89
|
+
def get_ordered_graphs(self):
|
|
90
|
+
"""Lee el historial de gráficos en el orden en que se generaron."""
|
|
91
|
+
history_file = os.path.join(self.base_path, "static", "graph_history.json")
|
|
92
|
+
|
|
93
|
+
if not os.path.exists(history_file):
|
|
94
|
+
return []
|
|
95
|
+
|
|
96
|
+
with open(history_file, "r") as f:
|
|
97
|
+
return json.load(f)
|
|
98
|
+
|
|
99
|
+
async def latest_image(self):
|
|
100
|
+
"""
|
|
101
|
+
Devuelve la lista de gráficos en formato JSON, manteniendo el orden de creación.
|
|
102
|
+
"""
|
|
103
|
+
graphs = self.get_ordered_graphs()
|
|
104
|
+
return JSONResponse(content={"graphs": graphs})
|
|
105
|
+
|
|
106
|
+
def notify_update(self):
|
|
107
|
+
if self.ws_connection is not None and self.loop is not None:
|
|
108
|
+
asyncio.run_coroutine_threadsafe(self.msg_notify_update(), self.loop)
|
|
109
|
+
|
|
110
|
+
async def msg_notify_update(self):
|
|
111
|
+
try:
|
|
112
|
+
await self.ws_connection.send_text("update")
|
|
113
|
+
except Exception:
|
|
114
|
+
self.ws_connection = None
|
|
115
|
+
|
|
116
|
+
def stop_server(self):
|
|
117
|
+
if self.server is not None:
|
|
118
|
+
self.start = False
|
|
119
|
+
self.server.should_exit = True
|
|
120
|
+
self.clear_graphs()
|
|
121
|
+
if hasattr(self, 'server_thread') and self.server_thread is not None:
|
|
122
|
+
self.server_thread.join()
|
|
123
|
+
print("Servidor detenido.")
|
|
124
|
+
|
|
125
|
+
def clear_graphs(self):
|
|
126
|
+
"""
|
|
127
|
+
Borra todo el contenido de la carpeta de imágenes y de gráficos interactivos.
|
|
128
|
+
"""
|
|
129
|
+
for folder in [self.image_folder, self.html_folder]:
|
|
130
|
+
if os.path.exists(folder):
|
|
131
|
+
for filename in os.listdir(folder):
|
|
132
|
+
file_path = os.path.join(folder, filename)
|
|
133
|
+
try:
|
|
134
|
+
if os.path.isfile(file_path) or os.path.islink(file_path):
|
|
135
|
+
os.unlink(file_path)
|
|
136
|
+
elif os.path.isdir(file_path):
|
|
137
|
+
shutil.rmtree(file_path)
|
|
138
|
+
except Exception as e:
|
|
139
|
+
print(f"No se pudo borrar {file_path}. Motivo: {e}")
|
|
140
|
+
|
|
141
|
+
async def rename_graph(self, request: Request):
|
|
142
|
+
data = await request.json()
|
|
143
|
+
old_title = data.get("old_title")
|
|
144
|
+
new_title = data.get("new_title")
|
|
145
|
+
|
|
146
|
+
history_file = os.path.join(self.base_path, "static", "graph_history.json")
|
|
147
|
+
if not os.path.exists(history_file):
|
|
148
|
+
return JSONResponse(content={"error": "No hay historial"}, status_code=404)
|
|
149
|
+
|
|
150
|
+
with open(history_file, "r") as f:
|
|
151
|
+
history = json.load(f)
|
|
152
|
+
|
|
153
|
+
updated = False
|
|
154
|
+
for graph in history:
|
|
155
|
+
if graph["title"] == old_title:
|
|
156
|
+
graph["title"] = new_title
|
|
157
|
+
updated = True
|
|
158
|
+
break
|
|
159
|
+
|
|
160
|
+
if updated:
|
|
161
|
+
with open(history_file, "w") as f:
|
|
162
|
+
json.dump(history, f)
|
|
163
|
+
return JSONResponse(content={"message": "Título actualizado"})
|
|
164
|
+
else:
|
|
165
|
+
return JSONResponse(content={"error": "Gráfico no encontrado"}, status_code=404)
|
pyvmote/__init__.py
ADDED
pyvmote/pyvmote.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from .Graph_generator import Graph
|
|
2
|
+
from .Service import Server
|
|
3
|
+
|
|
4
|
+
class Pyvmote:
|
|
5
|
+
def __init__(self):
|
|
6
|
+
self.gr = Graph()
|
|
7
|
+
self.sv = Server()
|
|
8
|
+
|
|
9
|
+
def start_server(self, puerto):
|
|
10
|
+
self.sv.start_server(puerto)
|
|
11
|
+
|
|
12
|
+
def stop_server(self):
|
|
13
|
+
self.sv.stop_server()
|
|
14
|
+
self.gr.clear_history()
|
|
15
|
+
|
|
16
|
+
def line_plot(self, x, y, xname="X", yname="Y", title="Line Graph", interactive=True, color='blue', linewidth=2, xlim=None, ylim=None):
|
|
17
|
+
plot_file = self.gr.line_plot(x, y, xname, yname, title, interactive, color, linewidth, xlim, ylim)
|
|
18
|
+
if self.sv.start:
|
|
19
|
+
self.sv.notify_update()
|
|
20
|
+
return plot_file
|
|
21
|
+
|
|
22
|
+
def scatter_plot(self, x, y, xname="X", yname="Y", title="Scatter Plot", interactive=True, color='blue', xlim=None, ylim=None):
|
|
23
|
+
plot_file = self.gr.scatter_plot(x, y, xname, yname, title, interactive, color, xlim, ylim)
|
|
24
|
+
if self.sv.start:
|
|
25
|
+
self.sv.notify_update()
|
|
26
|
+
return plot_file
|
|
27
|
+
|
|
28
|
+
def bar_plot(self, x, y, xname="X", yname="Y", title="Bar Plot", interactive=True, color='blue', xlim=None, ylim=None):
|
|
29
|
+
plot_file = self.gr.bar_plot(x, y, xname, yname, title, interactive, color, xlim, ylim)
|
|
30
|
+
if self.sv.start:
|
|
31
|
+
self.sv.notify_update()
|
|
32
|
+
return plot_file
|
|
33
|
+
|
|
34
|
+
def hist_plot(self, x, xname="Value", yname="Frequency", title="Histogram", bins=20, interactive=True, color='blue', xlim=None, ylim=None):
|
|
35
|
+
plot_file = self.gr.hist_plot(x, xname, yname, title, bins, interactive, color, xlim, ylim)
|
|
36
|
+
if self.sv.start:
|
|
37
|
+
self.sv.notify_update()
|
|
38
|
+
return plot_file
|
|
39
|
+
|
|
40
|
+
def box_plot(self, x, xname="", yname="Value", title="Box Plot", interactive=True):
|
|
41
|
+
plot_file = self.gr.box_plot(x, xname, yname, title, interactive)
|
|
42
|
+
if self.sv.start:
|
|
43
|
+
self.sv.notify_update()
|
|
44
|
+
return plot_file
|
|
45
|
+
|
|
46
|
+
def density_plot(self, x, xname="X", yname="Density", title="Density Plot", interactive=True, color='blue', xlim=None, ylim=None):
|
|
47
|
+
plot_file = self.gr.density_plot(x, xname, yname, title, interactive, color, xlim, ylim)
|
|
48
|
+
if self.sv.start:
|
|
49
|
+
self.sv.notify_update()
|
|
50
|
+
return plot_file
|
|
51
|
+
|
|
52
|
+
def pie_plot(self, sizes, labels=None, title="Pie Chart", interactive=True, colors=None):
|
|
53
|
+
plot_file = self.gr.pie_plot(sizes, labels, title, interactive, colors)
|
|
54
|
+
if self.sv.start:
|
|
55
|
+
self.sv.notify_update()
|
|
56
|
+
return plot_file
|
|
57
|
+
|
|
58
|
+
def cluster_plot(self, data, labels, title="Cluster Plot", interactive=True, cmap='viridis', xlim=None, ylim=None):
|
|
59
|
+
plot_file = self.gr.cluster_plot(data, labels, title, interactive, cmap, xlim, ylim)
|
|
60
|
+
if self.sv.start:
|
|
61
|
+
self.sv.notify_update()
|
|
62
|
+
return plot_file
|
|
63
|
+
|
|
64
|
+
def export_graph(self, title, extension="jpg", target_folder="exports"):
|
|
65
|
+
ruta = self.gr.save_as_format(title, extension, target_folder)
|
|
66
|
+
print(ruta)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyvmote
|
|
3
|
+
Version: 0.1.5
|
|
4
|
+
Summary: Librería para generar gráficos y visualizar imágenes en tiempo real en remoto
|
|
5
|
+
Author: Juan Grima Sanchez
|
|
6
|
+
Author-email: Juan Grima Sanchez <juan.grimasanchez5@gmail.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.7
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: fastapi
|
|
13
|
+
Requires-Dist: uvicorn[standard]
|
|
14
|
+
Requires-Dist: jinja2
|
|
15
|
+
Requires-Dist: matplotlib
|
|
16
|
+
Requires-Dist: mpld3
|
|
17
|
+
Requires-Dist: scipy
|
|
18
|
+
Requires-Dist: numpy
|
|
19
|
+
Dynamic: author
|
|
20
|
+
|
|
21
|
+
# 📊 PyVmote
|
|
22
|
+
|
|
23
|
+
**PyVmote** es una librería de Python para la **generación y visualización remota de gráficos**, tanto estáticos como interactivos, usando un servidor FastAPI. Permite visualizar gráficas directamente desde tu navegador incluso cuando trabajas en un entorno remoto (como SSH), gracias a su sistema de forwarding de puertos y WebSocket en tiempo real.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 🚀 Características principales
|
|
28
|
+
|
|
29
|
+
- 📈 Soporte para múltiples tipos de gráficos:
|
|
30
|
+
- Line plot
|
|
31
|
+
- Scatter plot
|
|
32
|
+
- Bar plot
|
|
33
|
+
- Histogram
|
|
34
|
+
- Boxplot
|
|
35
|
+
- Curvas de densidad (KDE)
|
|
36
|
+
|
|
37
|
+
- 🌐 Servidor web integrado con FastAPI
|
|
38
|
+
- ⚡ Recarga automática de gráficos mediante WebSocket
|
|
39
|
+
- 🌍 Visualización remota con un simple túnel SSH
|
|
40
|
+
- 🖱️ Soporte para gráficos **interactivos** con `mpld3`
|
|
41
|
+
- 📸 Exportación de gráficos a formatos `png`, `jpg`, `svg`, `pdf`, etc.
|
|
42
|
+
- 🧠 Historial de gráficos generado automáticamente
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 🚀 Fujo de trabajo
|
|
47
|
+
- import Pyvmote as p
|
|
48
|
+
- p.start_server(port) port exameple 3000
|
|
49
|
+
- p.line_plot(), p.scatter_plot(), p.box_plot(), p.bar_plot(), p.hist_plot(), p.density_plot() this will generate the graphs in the web server
|
|
50
|
+
- p.export_graph(title, extension, target_folder) title is also the name of the file image, extension = ["png", "jpg", "jpeg", "svg", "pdf"], target_folder = where do you want to save the photo
|
|
51
|
+
- p.stop_server()
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
pyvmote/Graph_generator.py,sha256=WDeuS0S0Ds6ESUeiaHJ4O2TtIY21BIHZIAKa_6pag0Q,9939
|
|
2
|
+
pyvmote/Service.py,sha256=_a69sXUsuWiNP25m66jitTR7BlYNeXBdEdi6wutS3Gg,6325
|
|
3
|
+
pyvmote/__init__.py,sha256=9Z92pgVaECSzp02o-Ma1qPPkKDeDYd6SpMVFA3d9kKY,221
|
|
4
|
+
pyvmote/pyvmote.py,sha256=CqmZaEM51cXKhvkBYt_LrT7-kXEPFF5gxumN1dvY1xI,2930
|
|
5
|
+
pyvmote-0.1.5.dist-info/METADATA,sha256=Sh1s-TvCx1YRl9T0KZAOYh0-L2uRT6iMIrnkWmB0e04,1953
|
|
6
|
+
pyvmote-0.1.5.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
|
|
7
|
+
pyvmote-0.1.5.dist-info/top_level.txt,sha256=ag8OwKUnIP9TDSNWL1zthhFrbe-FfYpjVpQ6b-fSEuk,8
|
|
8
|
+
pyvmote-0.1.5.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyvmote
|