Skip to content

Commit 50eb02c

Browse files
committed
Improve the online code editor
1 parent d4d7ddf commit 50eb02c

File tree

5 files changed

+155
-94
lines changed

5 files changed

+155
-94
lines changed

README.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ Example:
6060
6161
pip install raschii
6262
63+
You can also run Raschii in your web browser using PyScript/Pyodide, see the
64+
`online demo <https://raschii.readthedocs.io/en/latest/raschii_pyscript.html>`_!
65+
6366

6467
Using Raschii from Python
6568
.........................

documentation/_static/mini-coi.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/* See https://docs.pyscript.net/2025.7.3/user-guide/workers/ */
2+
/*! coi-serviceworker v0.1.7 - Guido Zuidhof and contributors, licensed under MIT */
3+
/*! mini-coi - Andrea Giammarchi and contributors, licensed under MIT */
4+
(({ document: d, navigator: { serviceWorker: s } }) => {
5+
if (d) {
6+
const { currentScript: c } = d;
7+
s.register(c.src, { scope: c.getAttribute('scope') || '.' }).then(r => {
8+
r.addEventListener('updatefound', () => location.reload());
9+
if (r.active && !s.controller) location.reload();
10+
});
11+
}
12+
else {
13+
addEventListener('install', () => skipWaiting());
14+
addEventListener('activate', e => e.waitUntil(clients.claim()));
15+
addEventListener('fetch', e => {
16+
const { request: r } = e;
17+
if (r.cache === 'only-if-cached' && r.mode !== 'same-origin') return;
18+
e.respondWith(fetch(r).then(r => {
19+
const { body, status, statusText } = r;
20+
if (!status || status > 399) return r;
21+
const h = new Headers(r.headers);
22+
h.set('Cross-Origin-Opener-Policy', 'same-origin');
23+
h.set('Cross-Origin-Embedder-Policy', 'require-corp');
24+
h.set('Cross-Origin-Resource-Policy', 'cross-origin');
25+
return new Response(body, { status, statusText, headers: h });
26+
}));
27+
});
28+
}
29+
})(self);

documentation/_static/raschii_pyscript.py

Lines changed: 85 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,23 @@ def plot_wave():
1212
1313
- Read the user input
1414
- Check the breaking criteria
15-
- Generate the wave,
16-
- Plots the wave using SVG
15+
- Generate the wave
16+
- Show the wave profile using SVG
1717
- Show the wave properties in output text area
1818
- Show warnings and errors in the info area
1919
2020
"""
21+
global current_raschii_wave
22+
current_raschii_wave = None
23+
2124
page.find("#raschii p.info").textContent = "Generating wave..."
2225
page.find("#raschii p.warning").textContent = ""
2326
page.find("#raschii p.error").textContent = ""
2427
page.find("#raschii_out").textContent = ""
25-
SvgWavePlot.current_wave = None
2628

2729
# Read the user input
2830
wave_input = WaveInput()
31+
wave_input.load_from_page()
2932
if not wave_input.is_ok:
3033
page.find("#raschii p.error").textContent = wave_input.error_message
3134
return
@@ -71,20 +74,25 @@ def plot_wave():
7174
f" Wave period = {wave.T:.2f}",
7275
f" Phase speed = {wave.c:.3f}",
7376
"",
74-
"Details:",
77+
"Numerical details:",
7578
*[f" {key}: {value}" for key, value in wave.data.items()],
7679
]
7780
page.find("#raschii_out").textContent = "\n".join(outputs)
78-
SvgWavePlot(wave=wave, x=x, eta=eta)
81+
current_raschii_wave = wave
82+
page.find("#raschii_plot").innerHTML = wave_profile_to_svg(x, eta)
7983

8084

8185
@pyscript.when("click", "#raschii_plot")
8286
def show_info_when_clicking_plot(mouse_event):
8387
"""
84-
This is called when the user clicks on the SVG plot.
88+
This is called when the user clicks on the SVG wave profile.
89+
90+
- Get the coordinates of the clicked point in the wave coordinate system
91+
- Show information about the clicked point, including particle velocities
92+
8593
"""
86-
wave = SvgWavePlot.current_wave
87-
if wave is None:
94+
global current_raschii_wave
95+
if current_raschii_wave is None:
8896
page.find("#raschii p.info").textContent = "No wave data available."
8997
return
9098

@@ -93,12 +101,12 @@ def show_info_when_clicking_plot(mouse_event):
93101

94102
# Show the information about the clicked point
95103
info = f"You clicked on x = {x:.3f} m, z = {z:.3f} m (from the bottom)"
96-
eta = wave.surface_elevation(x=[x], t=0.0)[0]
104+
eta = current_raschii_wave.surface_elevation(x=[x], t=0.0)[0]
97105
if z > eta:
98106
info += "<br>(Air)"
99107
else:
100108
info += "<br>(Water)"
101-
vel = wave.velocity(x, z, all_points_wet=True)
109+
vel = current_raschii_wave.velocity(x, z, all_points_wet=True)
102110
info += f"<br>Horizontal particle velocity: {vel[0, 0]:.3f}"
103111
info += f"<br>Vertical particle velocity: {vel[0, 1]:.3f}"
104112

@@ -107,37 +115,25 @@ def show_info_when_clicking_plot(mouse_event):
107115

108116
class WaveInput:
109117
def __init__(self):
118+
self.wave_model_name: str
119+
self.height: float
120+
self.depth: float
121+
self.length: float
122+
self.order: float
123+
110124
self.is_ok: bool = True
111125
self.warning_message: str = ""
112126
self.error_message: str = ""
113127

114-
def get_input_value(name: str, converter):
115-
try:
116-
element = page.find(f"#raschii_{name}")
117-
except Exception:
118-
self.is_ok = False
119-
self.error_message += f"Input element '{name}' not found.\n"
120-
return None
121-
122-
try:
123-
value = element[0].value
124-
except IndexError:
125-
self.is_ok = False
126-
self.error_message += f"Input element '{name}' is empty!\n"
127-
return None
128-
129-
try:
130-
return converter(value)
131-
except ValueError:
132-
self.is_ok = False
133-
self.error_message += f"Invalid value for '{name}': {value!r} is not a number!\n"
134-
return None
135-
136-
self.wave_model_name: str = get_input_value("wave_model", str)
137-
self.height: float = get_input_value("height", float)
138-
self.depth: float = get_input_value("depth", float)
139-
self.length: float = get_input_value("length", float)
140-
self.order: float = get_input_value("order", int)
128+
def load_from_page(self):
129+
"""
130+
Load the wave input parameters from the user input on the web page
131+
"""
132+
self.wave_model_name = self._get_page_input_value("wave_model", str)
133+
self.height = self._get_page_input_value("height", float)
134+
self.depth = self._get_page_input_value("depth", float)
135+
self.length = self._get_page_input_value("length", float)
136+
self.order = self._get_page_input_value("order", int)
141137

142138
if self.height < 0:
143139
self.is_ok = False
@@ -149,50 +145,64 @@ def get_input_value(name: str, converter):
149145
self.is_ok = False
150146
self.error_message += "Wave length must be positive!\n"
151147

148+
def _get_page_input_value(self, name: str, converter):
149+
try:
150+
element = page.find(f"#raschii_{name}")
151+
except Exception:
152+
self.is_ok = False
153+
self.error_message += f"Input element '{name}' not found.\n"
154+
return None
152155

153-
class SvgWavePlot:
154-
current_wave = None
156+
try:
157+
value = element[0].value
158+
except IndexError:
159+
self.is_ok = False
160+
self.error_message += f"Input element '{name}' is empty!\n"
161+
return None
155162

156-
def __init__(self, wave, x: list[float], eta: list[float]):
157-
SvgWavePlot.current_wave = wave
163+
try:
164+
return converter(value)
165+
except ValueError:
166+
self.is_ok = False
167+
self.error_message += f"Invalid value for '{name}': {value!r} is not a number!\n"
168+
return None
158169

159-
# Plot extents
160-
self.xmin = min(x)
161-
self.xmax = max(x)
162-
eta_min = min(eta)
163-
eta_max = max(eta)
164-
self.ymin = max(eta_min - (eta_max - eta_min) * 0.4, 0.0)
165-
self.ymax = eta_max + (eta_max - eta_min) * 0.4
166170

167-
self.x = [0.0, *x, x[-1]]
168-
self.y = [0.0, *eta, 0.0]
169-
self.eta = eta
170-
self.create_svg()
171+
def wave_profile_to_svg(x: list[float], eta: list[float]) -> str:
172+
"""
173+
Convert the wave profile to SVG format and returns it as string
171174
172-
def create_svg(self):
173-
"""
174-
Create the SVG tags to plot the wave.
175+
We need to handle the fact that SVG y-coordinates are inverted compared to
176+
the wave coordinate system. The SVG origin is at the top-left corner and is
177+
positive downwards, while the wave coordinate system has the origin at the
178+
still water surface and is positive upwards.
179+
"""
180+
# Plot extents
181+
xmin = min(x)
182+
xmax = max(x)
183+
eta_min = min(eta)
184+
eta_max = max(eta)
185+
ymin = max(eta_min - (eta_max - eta_min) * 0.4, 0.0)
186+
ymax = eta_max + (eta_max - eta_min) * 0.4
187+
188+
x_plot = [0.0, *x, x[-1]]
189+
y_plot = [0.0, *eta, 0.0]
190+
coords = " ".join(f"{x},{y}" for x, y in zip(x_plot, y_plot))
191+
192+
dx = xmax - xmin
193+
dy = ymax - ymin
194+
svg_contents = [
195+
'<svg version="1.1" preserveAspectRatio="none"',
196+
f'viewBox="{xmin} {-ymax} {dx} {dy}">',
197+
'<g transform="scale(1,-1)">',
198+
f'<path d="M{coords} z" fill="#687dc1"></path>',
199+
"</g>",
200+
"</svg>",
201+
]
202+
return "\n".join(svg_contents)
175203

176-
We need to handle the fact that SVG y-coordinates are inverted compared to
177-
the wave coordinate system. The SVG origin is at the top-left corner and is
178-
positive downwards, while the wave coordinate system has the origin at the
179-
still water surface and is positive upwards.
180-
"""
181-
dx = self.xmax - self.xmin
182-
dy = self.ymax - self.ymin
183-
coords = " ".join(f"{x},{y}" for x, y in zip(self.x, self.y))
184-
svg_contents = [
185-
'<svg version="1.1" preserveAspectRatio="none"',
186-
f'viewBox="{self.xmin} {-self.ymax} {dx} {dy}">',
187-
'<g transform="scale(1,-1)">',
188-
f'<path d="M{coords} z" fill="#687dc1"></path>',
189-
"</g>",
190-
"</svg>",
191-
]
192-
page.find("#raschii_plot").innerHTML = "\n".join(svg_contents)
193-
194-
195-
def get_physical_coordinates(mouse_event):
204+
205+
def get_physical_coordinates(mouse_event) -> tuple[float, float]:
196206
"""
197207
Convert the mouse click coordinates from the SVG element to the physical
198208
coordinates used when creating the SVG.

documentation/_static/raschii_pyscript_editor.html

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,35 @@
1010
<link rel="stylesheet" href="https://pyscript.net/releases/2025.7.3/core.css">
1111

1212
<!-- This script tag bootstraps PyScript -->
13+
<script src="./mini-coi.js"></script>
1314
<script type="module" src="https://pyscript.net/releases/2025.7.3/core.js"></script>
1415

1516
<title>Raschii Web editor using PyScript</title>
1617
</head>
1718

1819
<body>
19-
<script type="py-editor" config='{"packages":["raschii"]}'>
20+
<script type="py-editor" config='{"packages":["raschii", "matplotlib"]}'>
21+
from pyscript import display, web
22+
import numpy as np
23+
import matplotlib.pyplot as plt
2024
import raschii
2125

22-
fwave = raschii.FentonWave(height=0.25, depth=0.5, length=2.0, N=20)
23-
print(fwave.surface_elevation(x=0))
24-
print(fwave.surface_elevation(x=[0, 0.1, 0.2, 0.3]))
25-
print(fwave.velocity(x=0, z=0.2))
26+
wave = raschii.FentonWave(height=0.20, depth=0.5, length=2.0, N=10)
27+
28+
x = np.linspace(-wave.length, wave.length, 101)
29+
eta = wave.surface_elevation(x, t=0.0)
30+
31+
fig, ax = plt.subplots(figsize=(8, 4))
32+
ax.axhline(wave.depth, c='k', lw=1, ls=':')
33+
ax.fill_between(x, eta, eta*0, color='blue', alpha=0.5)
34+
35+
# Clear any prior output from the web page
36+
web.page["#raschii_editor_output"].innerHTML = ""
37+
38+
# Display the matplotlib figure
39+
display(fig, target="#raschii_editor_output")
2640
</script>
41+
<div id="raschii_editor_output"></div>
2742
</body>
2843

2944
</html>

documentation/raschii_pyscript.rst

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,21 @@ Online demo
55
===========================
66

77
If you are reading this in a web browser then a demo should be shown below.
8-
What you are then seeing is a Raschii running in the browser using
9-
`PyScript <https://pyscript.net/>`_ and `Pyodide <https://pyodide.org/>`_.
8+
This is Raschii running in your browser using `PyScript <https://pyscript.net/>`_
9+
and `Pyodide <https://pyodide.org/>`_.
10+
Note that some numerical calculations may give slightly different results than in
11+
"normal" Python running on your computer.
1012

13+
.. contents::
14+
:local:
1115

12-
Visualization
13-
=============
1416

15-
You can click the figure with the wave profile to see the particle velocities.
16-
It takes some time to download Python, numpy, and Raschii the first time you
17-
run this demo, so please be patient.
17+
Wave visualization
18+
==================
19+
20+
You can click the figure with the wave profile to see the particle velocities
21+
after the wave profile has been calculated.
22+
You can also `open in a full window <./_static/raschii_pyscript.html>`_.
1823

1924
.. raw:: html
2025

@@ -24,8 +29,8 @@ This web visualization was made after being inspired by the `original online
2429
wave calculators <http://www.coastal.udel.edu/faculty/rad/>`_ from the 1990s by
2530
Robert A. Dalrymple, specifically the `Dean stream function wave theory
2631
calculator <http://www.coastal.udel.edu/faculty/rad/streamless.html>`_.
27-
Unfortunately Dalrymple's calculators are based on Java applet technology which
28-
does not work well in modern web browsers (though, the JAR files can be
32+
Unfortunately, Dalrymple's calculators are based on Java applet technology which
33+
does not work well in modern web browsers (though the JAR files can be
2934
downloaded and run locally by a sufficiently technologically proficient user).
3035

3136
There was once a `port of Raschii to Dart <https://bitbucket.org/trlandet/raschiidart/>`_
@@ -36,9 +41,8 @@ longer maintained.
3641
Online code editor
3742
==================
3843

39-
You can also write your own code using Raschii (and numpy) in the below editor.
40-
Press the run (play) button to run the code.
44+
You can write and run your own code using Raschii, numpy, and matplotlib in the online code editor.
45+
Press the run (play) button to run the code; this button may not appear until you start editing.
46+
It will take a while to run the code for the first time, but after that it is relatively fast.
4147

42-
.. raw:: html
43-
44-
<iframe src="./_static/raschii_pyscript_editor.html" height="400px" width="100%"></iframe>
48+
`Open the online code editor <./_static/raschii_pyscript_editor.html>`_!

0 commit comments

Comments
 (0)