syd 0.1.4__py3-none-any.whl → 0.1.6__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.
@@ -0,0 +1,338 @@
1
+ from typing import Dict, Any, Optional
2
+ from dataclasses import dataclass
3
+ import threading
4
+ from flask import Flask, render_template_string, jsonify, request
5
+ import matplotlib
6
+
7
+ matplotlib.use("Agg") # Use Agg backend for thread safety
8
+ import matplotlib.pyplot as plt
9
+ import io
10
+ import base64
11
+ from contextlib import contextmanager
12
+ import webbrowser
13
+
14
+ from ..interactive_viewer import InteractiveViewer
15
+ from .components import WebComponentCollection
16
+
17
+ # Create a template constant to hold our HTML template
18
+ PAGE_TEMPLATE = """
19
+ <!DOCTYPE html>
20
+ <html>
21
+ <head>
22
+ <title>Interactive Viewer</title>
23
+ {% for css in required_css %}
24
+ <link rel="stylesheet" href="{{ css }}">
25
+ {% endfor %}
26
+ <style>
27
+ {{ custom_styles | safe }}
28
+ .controls {
29
+ {% if config.is_horizontal %}
30
+ width: {{ config.controls_width_percent }}%;
31
+ float: left;
32
+ padding-right: 20px;
33
+ {% endif %}
34
+ }
35
+ .plot-container {
36
+ {% if config.is_horizontal %}
37
+ width: {{ 100 - config.controls_width_percent }}%;
38
+ float: left;
39
+ {% endif %}
40
+ }
41
+ #plot {
42
+ width: 100%;
43
+ height: auto;
44
+ }
45
+ </style>
46
+ </head>
47
+ <body>
48
+ <div class="container-fluid">
49
+ <div class="row">
50
+ <div class="controls">
51
+ {{ components_html | safe }}
52
+ </div>
53
+ <div class="plot-container">
54
+ <img id="plot" src="{{ initial_plot }}">
55
+ </div>
56
+ </div>
57
+ </div>
58
+
59
+ {% for js in required_js %}
60
+ <script src="{{ js }}"></script>
61
+ {% endfor %}
62
+
63
+ <script>
64
+ function updateParameter(name, value) {
65
+ fetch('/update_parameter', {
66
+ method: 'POST',
67
+ headers: {
68
+ 'Content-Type': 'application/json',
69
+ },
70
+ body: JSON.stringify({name, value})
71
+ })
72
+ .then(response => response.json())
73
+ .then(data => {
74
+ if (data.error) {
75
+ console.error(data.error);
76
+ return;
77
+ }
78
+ // Update plot
79
+ document.getElementById('plot').src = data.plot;
80
+ // Apply any parameter updates
81
+ for (const [param, js] of Object.entries(data.updates)) {
82
+ eval(js);
83
+ }
84
+ });
85
+ }
86
+
87
+ function buttonClick(name) {
88
+ fetch('/button_click', {
89
+ method: 'POST',
90
+ headers: {
91
+ 'Content-Type': 'application/json',
92
+ },
93
+ body: JSON.stringify({name})
94
+ })
95
+ .then(response => response.json())
96
+ .then(data => {
97
+ if (data.error) {
98
+ console.error(data.error);
99
+ return;
100
+ }
101
+ // Update plot
102
+ document.getElementById('plot').src = data.plot;
103
+ // Apply any parameter updates
104
+ for (const [param, js] of Object.entries(data.updates)) {
105
+ eval(js);
106
+ }
107
+ });
108
+ }
109
+
110
+ // Initialize components
111
+ {{ components_init | safe }}
112
+ </script>
113
+ </body>
114
+ </html>
115
+ """
116
+
117
+
118
+ @dataclass
119
+ class FlaskLayoutConfig:
120
+ """Configuration for the viewer layout in Flask deployment."""
121
+
122
+ controls_position: str = "left" # Options are: 'left', 'top'
123
+ figure_width: float = 8.0
124
+ figure_height: float = 6.0
125
+ controls_width_percent: int = 30
126
+ port: int = 5000
127
+ host: str = "localhost"
128
+
129
+ def __post_init__(self):
130
+ valid_positions = ["left", "top"]
131
+ if self.controls_position not in valid_positions:
132
+ raise ValueError(
133
+ f"Invalid controls position: {self.controls_position}. Must be one of {valid_positions}"
134
+ )
135
+
136
+ @property
137
+ def is_horizontal(self) -> bool:
138
+ return self.controls_position == "left"
139
+
140
+
141
+ class FlaskDeployment:
142
+ """A deployment system for InteractiveViewer using Flask to create a web interface."""
143
+
144
+ def __init__(
145
+ self,
146
+ viewer: InteractiveViewer,
147
+ layout_config: Optional[FlaskLayoutConfig] = None,
148
+ ):
149
+ if not isinstance(viewer, InteractiveViewer):
150
+ raise TypeError(
151
+ f"viewer must be an InteractiveViewer, got {type(viewer).__name__}"
152
+ )
153
+
154
+ self.viewer = viewer
155
+ self.config = layout_config or FlaskLayoutConfig()
156
+ self.components = WebComponentCollection()
157
+
158
+ # Initialize Flask app
159
+ self.app = Flask(__name__)
160
+ self._setup_routes()
161
+
162
+ # Store current figure
163
+ self._current_figure = None
164
+ self._figure_lock = threading.Lock()
165
+
166
+ def _setup_routes(self):
167
+ """Set up the Flask routes for the application."""
168
+
169
+ @self.app.route("/")
170
+ def index():
171
+ return self._render_page()
172
+
173
+ @self.app.route("/update_parameter", methods=["POST"])
174
+ def update_parameter():
175
+ data = request.json
176
+ name = data.get("name")
177
+ value = data.get("value")
178
+
179
+ print(f"Received parameter update: {name} = {value}") # Debug log
180
+
181
+ if name not in self.viewer.parameters:
182
+ print(f"Parameter {name} not found in viewer parameters") # Debug log
183
+ return jsonify({"error": f"Parameter {name} not found"}), 404
184
+
185
+ try:
186
+ print(f"Setting parameter value: {name} = {value}") # Debug log
187
+ self.viewer.set_parameter_value(name, value)
188
+
189
+ # Update the plot with new parameter values
190
+ print("Updating plot with new parameters...") # Debug log
191
+ self._update_plot()
192
+
193
+ updates = self._get_parameter_updates()
194
+ plot_data = self._get_current_plot_data()
195
+ # Debug log
196
+ print(f"Generated updates for parameters: {list(updates.keys())}")
197
+
198
+ return jsonify(
199
+ {
200
+ "success": True,
201
+ "updates": updates,
202
+ "plot": plot_data,
203
+ }
204
+ )
205
+ except Exception as e:
206
+ print(f"Error updating parameter: {str(e)}") # Debug log
207
+ return jsonify({"error": str(e)}), 400
208
+
209
+ @self.app.route("/button_click", methods=["POST"])
210
+ def button_click():
211
+ data = request.json
212
+ name = data.get("name")
213
+
214
+ print(f"Received button click: {name}") # Debug log
215
+
216
+ if name not in self.viewer.parameters:
217
+ print(f"Button {name} not found in viewer parameters") # Debug log
218
+ return jsonify({"error": f"Parameter {name} not found"}), 404
219
+
220
+ try:
221
+ parameter = self.viewer.parameters[name]
222
+ if hasattr(parameter, "callback"):
223
+ print(f"Executing callback for button: {name}") # Debug log
224
+ parameter.callback(self.viewer.get_state())
225
+ else:
226
+ print(f"No callback found for button: {name}") # Debug log
227
+
228
+ # Update the plot after button click
229
+ print("Updating plot after button click...") # Debug log
230
+ self._update_plot()
231
+
232
+ updates = self._get_parameter_updates()
233
+ plot_data = self._get_current_plot_data()
234
+ # Debug log
235
+ print(f"Generated updates for parameters: {list(updates.keys())}")
236
+
237
+ return jsonify(
238
+ {
239
+ "success": True,
240
+ "updates": updates,
241
+ "plot": plot_data,
242
+ }
243
+ )
244
+ except Exception as e:
245
+ print(f"Error handling button click: {str(e)}") # Debug log
246
+ return jsonify({"error": str(e)}), 400
247
+
248
+ def _create_components(self):
249
+ """Create web components for all parameters."""
250
+ for name, param in self.viewer.parameters.items():
251
+ self.components.add_component(name, param)
252
+
253
+ @contextmanager
254
+ def _plot_context(self):
255
+ """Context manager for thread-safe plotting."""
256
+ plt.ioff()
257
+ try:
258
+ yield
259
+ finally:
260
+ plt.ion()
261
+
262
+ def _update_plot(self) -> None:
263
+ """Update the plot with current state."""
264
+ state = self.viewer.get_state()
265
+ print(f"Updating plot with state: {state}") # Debug log
266
+
267
+ with self._plot_context(), self._figure_lock:
268
+ new_fig = self.viewer.plot(state)
269
+ plt.close(self._current_figure) # Close old figure
270
+ self._current_figure = new_fig
271
+ print("Plot updated successfully") # Debug log
272
+
273
+ def _get_current_plot_data(self) -> str:
274
+ """Get the current plot as a base64 encoded PNG."""
275
+ with self._figure_lock:
276
+ if self._current_figure is None:
277
+ return ""
278
+
279
+ buffer = io.BytesIO()
280
+ self._current_figure.savefig(
281
+ buffer,
282
+ format="png",
283
+ bbox_inches="tight",
284
+ dpi=300,
285
+ )
286
+ buffer.seek(0)
287
+ image_png = buffer.getvalue()
288
+ buffer.close()
289
+
290
+ graphic = base64.b64encode(image_png).decode("utf-8")
291
+ return f"data:image/png;base64,{graphic}"
292
+
293
+ def _get_parameter_updates(self) -> Dict[str, Any]:
294
+ """Get JavaScript updates for all parameters."""
295
+ updates = {}
296
+ state = self.viewer.get_state()
297
+ for name, value in state.items():
298
+ # Skip button parameters since they don't have a meaningful value to update
299
+ if (
300
+ hasattr(self.viewer.parameters[name], "_is_button")
301
+ and self.viewer.parameters[name]._is_button
302
+ ):
303
+ continue
304
+ updates[name] = self.components.get_update_js(name, value)
305
+ return updates
306
+
307
+ def _render_page(self) -> str:
308
+ """Render the complete HTML page."""
309
+ # Create initial plot
310
+ self._update_plot()
311
+
312
+ return render_template_string(
313
+ PAGE_TEMPLATE,
314
+ config=self.config,
315
+ components_html=self.components.get_all_html(),
316
+ components_init=self.components.get_init_js(),
317
+ initial_plot=self._get_current_plot_data(),
318
+ required_css=self.components.get_required_css(),
319
+ required_js=self.components.get_required_js(),
320
+ custom_styles=self.components.get_custom_styles(),
321
+ )
322
+
323
+ def deploy(self) -> None:
324
+ """Deploy the interactive viewer as a web application."""
325
+ with self.viewer._deploy_app():
326
+ # Create components
327
+ self._create_components()
328
+
329
+ # Open browser
330
+ webbrowser.open(f"http://{self.config.host}:{self.config.port}")
331
+
332
+ # Start Flask app
333
+ self.app.run(
334
+ host=self.config.host,
335
+ port=self.config.port,
336
+ debug=False, # Debug mode doesn't work well with matplotlib
337
+ use_reloader=False, # Prevent double startup
338
+ )
@@ -0,0 +1,39 @@
1
+ .controls {
2
+ padding: 20px;
3
+ }
4
+
5
+ .controls.horizontal {
6
+ width: var(--controls-width);
7
+ float: left;
8
+ padding-right: 20px;
9
+ }
10
+
11
+ .plot-container {
12
+ padding: 20px;
13
+ }
14
+
15
+ .plot-container.horizontal {
16
+ width: var(--plot-width);
17
+ float: left;
18
+ }
19
+
20
+ #plot {
21
+ width: 100%;
22
+ height: auto;
23
+ }
24
+
25
+ .slider, .range-slider {
26
+ margin: 10px 0;
27
+ }
28
+
29
+ .value-display {
30
+ display: block;
31
+ text-align: center;
32
+ margin-top: 5px;
33
+ font-size: 0.9em;
34
+ color: #666;
35
+ }
36
+
37
+ .form-group {
38
+ margin-bottom: 1rem;
39
+ }
@@ -0,0 +1,51 @@
1
+ function updateParameter(name, value) {
2
+ console.log(`Sending parameter update: ${name} = ${value}`); // Debug log
3
+ fetch('/update_parameter', {
4
+ method: 'POST',
5
+ headers: {
6
+ 'Content-Type': 'application/json',
7
+ },
8
+ body: JSON.stringify({name, value})
9
+ })
10
+ .then(response => response.json())
11
+ .then(data => {
12
+ if (data.error) {
13
+ console.error(`Error updating parameter: ${data.error}`); // Debug log
14
+ return;
15
+ }
16
+ console.log('Update successful, applying updates'); // Debug log
17
+ // Update plot
18
+ document.getElementById('plot').src = data.plot;
19
+ // Apply any parameter updates
20
+ for (const [param, js] of Object.entries(data.updates)) {
21
+ console.log(`Applying update for ${param}`); // Debug log
22
+ eval(js);
23
+ }
24
+ })
25
+ .catch(error => {
26
+ console.error('Error in updateParameter:', error); // Debug log
27
+ });
28
+ }
29
+
30
+ function buttonClick(name) {
31
+ fetch('/button_click', {
32
+ method: 'POST',
33
+ headers: {
34
+ 'Content-Type': 'application/json',
35
+ },
36
+ body: JSON.stringify({name})
37
+ })
38
+ .then(response => response.json())
39
+ .then(data => {
40
+ if (data.error) {
41
+ console.error(data.error);
42
+ return;
43
+ }
44
+ // Update plot
45
+ document.getElementById('plot').src = data.plot;
46
+ // Apply any parameter updates
47
+ for (const [param, js] of Object.entries(data.updates)) {
48
+ eval(js);
49
+ }
50
+ });
51
+ }
File without changes
@@ -0,0 +1,26 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>{% block title %}Interactive Viewer{% endblock %}</title>
5
+ {% for css in required_css %}
6
+ <link rel="stylesheet" href="{{ css }}">
7
+ {% endfor %}
8
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
9
+ <style>
10
+ :root {
11
+ --controls-width: {{ config.controls_width_percent }}%;
12
+ --plot-width: {{ 100 - config.controls_width_percent }}%;
13
+ }
14
+ {{ custom_styles | safe }}
15
+ </style>
16
+ </head>
17
+ <body>
18
+ {% block content %}{% endblock %}
19
+
20
+ {% for js in required_js %}
21
+ <script src="{{ js }}"></script>
22
+ {% endfor %}
23
+ <script src="{{ url_for('static', filename='js/components.js') }}"></script>
24
+ {% block scripts %}{% endblock %}
25
+ </body>
26
+ </html>
@@ -0,0 +1,97 @@
1
+ # templates/viewer.html
2
+ <!DOCTYPE html>
3
+ <html>
4
+ <head>
5
+ <title>Interactive Viewer</title>
6
+ {% for css in required_css %}
7
+ <link rel="stylesheet" href="{{ css }}">
8
+ {% endfor %}
9
+ <style>
10
+ {{ custom_styles | safe }}
11
+ .controls {
12
+ {% if config.is_horizontal %}
13
+ width: {{ config.controls_width_percent }}%;
14
+ float: left;
15
+ padding-right: 20px;
16
+ {% endif %}
17
+ }
18
+ .plot-container {
19
+ {% if config.is_horizontal %}
20
+ width: {{ 100 - config.controls_width_percent }}%;
21
+ float: left;
22
+ {% endif %}
23
+ }
24
+ #plot {
25
+ width: 100%;
26
+ height: auto;
27
+ }
28
+ </style>
29
+ </head>
30
+ <body>
31
+ <div class="container-fluid">
32
+ <div class="row">
33
+ <div class="controls">
34
+ {{ components_html | safe }}
35
+ </div>
36
+ <div class="plot-container">
37
+ <img id="plot" src="{{ initial_plot }}">
38
+ </div>
39
+ </div>
40
+ </div>
41
+
42
+ {% for js in required_js %}
43
+ <script src="{{ js }}"></script>
44
+ {% endfor %}
45
+
46
+ <script>
47
+ function updateParameter(name, value) {
48
+ fetch('/update_parameter', {
49
+ method: 'POST',
50
+ headers: {
51
+ 'Content-Type': 'application/json',
52
+ },
53
+ body: JSON.stringify({name, value})
54
+ })
55
+ .then(response => response.json())
56
+ .then(data => {
57
+ if (data.error) {
58
+ console.error(data.error);
59
+ return;
60
+ }
61
+ // Update plot
62
+ document.getElementById('plot').src = data.plot;
63
+ // Apply any parameter updates
64
+ for (const [param, js] of Object.entries(data.updates)) {
65
+ eval(js);
66
+ }
67
+ });
68
+ }
69
+
70
+ function buttonClick(name) {
71
+ fetch('/button_click', {
72
+ method: 'POST',
73
+ headers: {
74
+ 'Content-Type': 'application/json',
75
+ },
76
+ body: JSON.stringify({name})
77
+ })
78
+ .then(response => response.json())
79
+ .then(data => {
80
+ if (data.error) {
81
+ console.error(data.error);
82
+ return;
83
+ }
84
+ // Update plot
85
+ document.getElementById('plot').src = data.plot;
86
+ // Apply any parameter updates
87
+ for (const [param, js] of Object.entries(data.updates)) {
88
+ eval(js);
89
+ }
90
+ });
91
+ }
92
+
93
+ // Initialize components
94
+ {{ components_init | safe }}
95
+ </script>
96
+ </body>
97
+ </html>