Spaces:
Running
Running
thibaud frere
commited on
Commit
·
5846c4a
1
Parent(s):
3f64d97
feat(fragments): add HtmlFragment component and example fragments/banner.html
Browse files- app/src/components/HtmlFragment.astro +27 -0
- app/src/fragments/banner.html +13 -0
- app/src/pages/index.astro +5 -8
- python/convert.py +32 -0
- python/convert_to_md.py +110 -0
- python/fragments/banner.py +100 -0
- python/fragments/bar.py +131 -0
- python/fragments/heatmap.py +122 -0
- python/fragments/line.py +243 -0
app/src/components/HtmlFragment.astro
ADDED
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
interface Props { src: string }
|
3 |
+
const { src } = Astro.props as Props;
|
4 |
+
|
5 |
+
// Charge tous les fragments .html sous src/fragments/** en tant que string (dev & build)
|
6 |
+
const fragments = import.meta.glob('../fragments/**/*.html', { as: 'raw', eager: true }) as Record<string, string>;
|
7 |
+
|
8 |
+
function resolveFragment(requested: string): string | null {
|
9 |
+
// Autorise "banner.html" ou "fragments/banner.html"
|
10 |
+
const needle = requested.replace(/^\/*/, '');
|
11 |
+
for (const [key, html] of Object.entries(fragments)) {
|
12 |
+
if (key.endsWith('/' + needle) || key.endsWith('/' + needle.replace(/^fragments\//, ''))) {
|
13 |
+
return html;
|
14 |
+
}
|
15 |
+
}
|
16 |
+
return null;
|
17 |
+
}
|
18 |
+
|
19 |
+
const html = resolveFragment(src);
|
20 |
+
---
|
21 |
+
{ html ? (
|
22 |
+
<div set:html={html} />
|
23 |
+
) : (
|
24 |
+
<div><!-- Fragment introuvable: {src} --></div>
|
25 |
+
) }
|
26 |
+
|
27 |
+
|
app/src/fragments/banner.html
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<div id="fragment-banner" style="max-width: 980px; margin: 0 auto;">
|
2 |
+
<div id="banner-plot" style="width:100%;height:360px;"></div>
|
3 |
+
<script>
|
4 |
+
if (window.Plotly && document.getElementById('banner-plot')) {
|
5 |
+
const el = document.getElementById('banner-plot');
|
6 |
+
const x = Array.from({length: 200}, (_, i) => i/10);
|
7 |
+
const y = x.map(v => Math.sin(v));
|
8 |
+
window.Plotly.newPlot(el, [{x, y, mode:'lines'}], {margin:{t:16,r:16,b:32,l:32}});
|
9 |
+
}
|
10 |
+
</script>
|
11 |
+
</div>
|
12 |
+
|
13 |
+
|
app/src/pages/index.astro
CHANGED
@@ -1,6 +1,7 @@
|
|
1 |
---
|
2 |
import { Image } from 'astro:assets';
|
3 |
import banner from "../assets/images/banner.png";
|
|
|
4 |
const title = 'The Distill Blog Template (Astro)';
|
5 |
---
|
6 |
<html lang="en">
|
@@ -45,6 +46,9 @@ const title = 'The Distill Blog Template (Astro)';
|
|
45 |
<Image src={banner} alt="Banner" widths={[480,768,1080,1440]} formats={["avif","webp","png"]} sizes="(max-width: 768px) 100vw, var(--page-width)" loading="eager" />
|
46 |
</figure>
|
47 |
</div>
|
|
|
|
|
|
|
48 |
<p style="text-align: center; font-style: italic; margin-top: 10px; max-width: 900px; margin-left: auto; margin-right: auto;">It's nice to have a cute interactive banner!</p>
|
49 |
</div>
|
50 |
</d-title>
|
@@ -85,15 +89,8 @@ const title = 'The Distill Blog Template (Astro)';
|
|
85 |
|
86 |
<h2>Interactive Components</h2>
|
87 |
<div class="plot-card">
|
88 |
-
<
|
89 |
</div>
|
90 |
-
<script>
|
91 |
-
// Example: simple Plotly line chart using the CDN
|
92 |
-
const container = document.getElementById('fragment-line');
|
93 |
-
if (container && window.Plotly) {
|
94 |
-
window.Plotly.newPlot(container, [{x:[1,2,3,4], y:[1,3,2,4], type:'scatter'}], {margin:{t:16,r:16,b:32,l:32}});
|
95 |
-
}
|
96 |
-
</script>
|
97 |
|
98 |
</d-article>
|
99 |
|
|
|
1 |
---
|
2 |
import { Image } from 'astro:assets';
|
3 |
import banner from "../assets/images/banner.png";
|
4 |
+
import HtmlFragment from "../components/HtmlFragment.astro";
|
5 |
const title = 'The Distill Blog Template (Astro)';
|
6 |
---
|
7 |
<html lang="en">
|
|
|
46 |
<Image src={banner} alt="Banner" widths={[480,768,1080,1440]} formats={["avif","webp","png"]} sizes="(max-width: 768px) 100vw, var(--page-width)" loading="eager" />
|
47 |
</figure>
|
48 |
</div>
|
49 |
+
<div class="l-page" style="margin-top:12px;">
|
50 |
+
<HtmlFragment src="banner.html" />
|
51 |
+
</div>
|
52 |
<p style="text-align: center; font-style: italic; margin-top: 10px; max-width: 900px; margin-left: auto; margin-right: auto;">It's nice to have a cute interactive banner!</p>
|
53 |
</div>
|
54 |
</d-title>
|
|
|
89 |
|
90 |
<h2>Interactive Components</h2>
|
91 |
<div class="plot-card">
|
92 |
+
<HtmlFragment src="banner.html" />
|
93 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
94 |
|
95 |
</d-article>
|
96 |
|
python/convert.py
ADDED
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
import markdown
|
3 |
+
from pathlib import Path
|
4 |
+
import sys
|
5 |
+
|
6 |
+
def convert_md_to_html(filepath):
|
7 |
+
input_path = Path(filepath)
|
8 |
+
output_path = input_path.with_suffix('.html')
|
9 |
+
|
10 |
+
try:
|
11 |
+
with open(input_path, 'r', encoding='utf-8') as md_file:
|
12 |
+
text = md_file.read()
|
13 |
+
html = markdown.markdown(text)
|
14 |
+
|
15 |
+
with open(output_path, 'w', encoding='utf-8', errors='xmlcharrefreplace') as html_file:
|
16 |
+
html_file.write(html)
|
17 |
+
|
18 |
+
print(f"Converted {input_path} -> {output_path}")
|
19 |
+
|
20 |
+
except FileNotFoundError:
|
21 |
+
print(f"Error: Could not find file {input_path}")
|
22 |
+
sys.exit(1)
|
23 |
+
except Exception as e:
|
24 |
+
print(f"Error converting file: {e}")
|
25 |
+
sys.exit(1)
|
26 |
+
|
27 |
+
if __name__ == '__main__':
|
28 |
+
if len(sys.argv) != 2:
|
29 |
+
print("Usage: python convert.py FILEPATH.md")
|
30 |
+
sys.exit(1)
|
31 |
+
|
32 |
+
convert_md_to_html(sys.argv[1])
|
python/convert_to_md.py
ADDED
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
"""
|
3 |
+
HTML to Markdown Converter
|
4 |
+
|
5 |
+
This script converts HTML files to Markdown format.
|
6 |
+
Usage: python html_to_md.py input.html [output.md]
|
7 |
+
If no output file is specified, it will use the input filename with .md extension.
|
8 |
+
"""
|
9 |
+
|
10 |
+
import sys
|
11 |
+
import os
|
12 |
+
import argparse
|
13 |
+
import html2text
|
14 |
+
import requests
|
15 |
+
from urllib.parse import urlparse
|
16 |
+
|
17 |
+
def is_url(path):
|
18 |
+
"""Check if the given path is a URL."""
|
19 |
+
parsed = urlparse(path)
|
20 |
+
return parsed.scheme != '' and parsed.netloc != ''
|
21 |
+
|
22 |
+
def convert_html_to_markdown(html_content, **options):
|
23 |
+
"""Convert HTML content to Markdown."""
|
24 |
+
converter = html2text.HTML2Text()
|
25 |
+
|
26 |
+
# Configure converter options
|
27 |
+
converter.ignore_links = options.get('ignore_links', False)
|
28 |
+
converter.ignore_images = options.get('ignore_images', False)
|
29 |
+
converter.ignore_tables = options.get('ignore_tables', False)
|
30 |
+
converter.body_width = options.get('body_width', 0) # 0 means no wrapping
|
31 |
+
converter.unicode_snob = options.get('unicode_snob', True) # Use Unicode instead of ASCII
|
32 |
+
converter.wrap_links = options.get('wrap_links', False)
|
33 |
+
converter.inline_links = options.get('inline_links', True)
|
34 |
+
|
35 |
+
# Convert HTML to Markdown
|
36 |
+
return converter.handle(html_content)
|
37 |
+
|
38 |
+
def main():
|
39 |
+
parser = argparse.ArgumentParser(description='Convert HTML to Markdown')
|
40 |
+
parser.add_argument('input', help='Input HTML file or URL')
|
41 |
+
parser.add_argument('output', nargs='?', help='Output Markdown file (optional)')
|
42 |
+
parser.add_argument('--ignore-links', action='store_true', help='Ignore links in the HTML')
|
43 |
+
parser.add_argument('--ignore-images', action='store_true', help='Ignore images in the HTML')
|
44 |
+
parser.add_argument('--ignore-tables', action='store_true', help='Ignore tables in the HTML')
|
45 |
+
parser.add_argument('--body-width', type=int, default=0, help='Wrap text at this width (0 for no wrapping)')
|
46 |
+
parser.add_argument('--unicode', action='store_true', help='Use Unicode characters instead of ASCII approximations')
|
47 |
+
parser.add_argument('--wrap-links', action='store_true', help='Wrap links in angle brackets')
|
48 |
+
parser.add_argument('--reference-links', action='store_true', help='Use reference style links instead of inline links')
|
49 |
+
|
50 |
+
args = parser.parse_args()
|
51 |
+
|
52 |
+
# Determine input
|
53 |
+
if is_url(args.input):
|
54 |
+
try:
|
55 |
+
response = requests.get(args.input)
|
56 |
+
response.raise_for_status()
|
57 |
+
html_content = response.text
|
58 |
+
except requests.exceptions.RequestException as e:
|
59 |
+
print(f"Error fetching URL: {e}", file=sys.stderr)
|
60 |
+
return 1
|
61 |
+
else:
|
62 |
+
try:
|
63 |
+
with open(args.input, 'r', encoding='utf-8') as f:
|
64 |
+
html_content = f.read()
|
65 |
+
except IOError as e:
|
66 |
+
print(f"Error reading file: {e}", file=sys.stderr)
|
67 |
+
return 1
|
68 |
+
|
69 |
+
# Configure conversion options
|
70 |
+
options = {
|
71 |
+
'ignore_links': args.ignore_links,
|
72 |
+
'ignore_images': args.ignore_images,
|
73 |
+
'ignore_tables': args.ignore_tables,
|
74 |
+
'body_width': args.body_width,
|
75 |
+
'unicode_snob': args.unicode,
|
76 |
+
'wrap_links': args.wrap_links,
|
77 |
+
'inline_links': not args.reference_links,
|
78 |
+
}
|
79 |
+
|
80 |
+
# Convert HTML to Markdown
|
81 |
+
markdown_content = convert_html_to_markdown(html_content, **options)
|
82 |
+
|
83 |
+
# Determine output
|
84 |
+
if args.output:
|
85 |
+
output_file = args.output
|
86 |
+
else:
|
87 |
+
if is_url(args.input):
|
88 |
+
# Generate a filename from the URL
|
89 |
+
url_parts = urlparse(args.input)
|
90 |
+
base_name = os.path.basename(url_parts.path) or 'index'
|
91 |
+
if not base_name.endswith('.html'):
|
92 |
+
base_name += '.html'
|
93 |
+
output_file = os.path.splitext(base_name)[0] + '.md'
|
94 |
+
else:
|
95 |
+
# Generate a filename from the input file
|
96 |
+
output_file = os.path.splitext(args.input)[0] + '.md'
|
97 |
+
|
98 |
+
# Write output
|
99 |
+
try:
|
100 |
+
with open(output_file, 'w', encoding='utf-8') as f:
|
101 |
+
f.write(markdown_content)
|
102 |
+
print(f"Conversion successful! Output saved to: {output_file}")
|
103 |
+
except IOError as e:
|
104 |
+
print(f"Error writing file: {e}", file=sys.stderr)
|
105 |
+
return 1
|
106 |
+
|
107 |
+
return 0
|
108 |
+
|
109 |
+
if __name__ == "__main__":
|
110 |
+
sys.exit(main())
|
python/fragments/banner.py
ADDED
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import plotly.graph_objects as go
|
2 |
+
import numpy as np
|
3 |
+
import pandas as pd
|
4 |
+
|
5 |
+
# Paramètres de l'ellipse (galaxie) et échantillonnage
|
6 |
+
num_points = 512
|
7 |
+
cx, cy = 1.5, 0.5 # centre (au milieu des ranges actuels)
|
8 |
+
a, b = 1.3, 0.45 # demi‑axes (ellipse horizontale)
|
9 |
+
|
10 |
+
# Échantillonnage en coordonnées polaires puis transformation elliptique
|
11 |
+
# r concentré vers le centre (alpha>1) pour densité centrale façon galaxie
|
12 |
+
theta = 2*np.pi*np.random.rand(num_points)
|
13 |
+
r_base = np.random.rand(num_points)**2
|
14 |
+
|
15 |
+
# Légère irrégularité pour un aspect plus naturel
|
16 |
+
noise_x = 0.015*np.random.randn(num_points)
|
17 |
+
noise_y = 0.015*np.random.randn(num_points)
|
18 |
+
|
19 |
+
x = cx + a * r_base * np.cos(theta) + noise_x
|
20 |
+
y = cy + b * r_base * np.sin(theta) + noise_y
|
21 |
+
|
22 |
+
# Taille plus grande au centre, plus petite en périphérie
|
23 |
+
# On conserve la même échelle finale qu'avant: (valeur in [0,1]) -> (val+1)*5
|
24 |
+
z_raw = 1 - r_base # 1 au centre, 0 au bord
|
25 |
+
sizes = (z_raw + 1) * 5 # 5..10, comme précédemment
|
26 |
+
|
27 |
+
df = pd.DataFrame({
|
28 |
+
"x": x,
|
29 |
+
"y": y,
|
30 |
+
"z": sizes, # réutilisé pour size+color comme avant
|
31 |
+
})
|
32 |
+
|
33 |
+
def get_label(z):
|
34 |
+
if z<0.25:
|
35 |
+
return "smol dot"
|
36 |
+
if z<0.5:
|
37 |
+
return "ok-ish dot"
|
38 |
+
if z<0.75:
|
39 |
+
return "a dot"
|
40 |
+
else:
|
41 |
+
return "biiig dot"
|
42 |
+
|
43 |
+
# Les labels sont fondés sur l'intensité centrale (z_raw en [0,1])
|
44 |
+
df["label"] = pd.Series(z_raw).apply(get_label)
|
45 |
+
|
46 |
+
fig = go.Figure()
|
47 |
+
|
48 |
+
fig.add_trace(go.Scatter(
|
49 |
+
x=df['x'],
|
50 |
+
y=df['y'],
|
51 |
+
mode='markers',
|
52 |
+
marker=dict(
|
53 |
+
size=df['z'],
|
54 |
+
color=df['z'],
|
55 |
+
colorscale=[
|
56 |
+
[0, 'rgb(78, 165, 183)'], # Light blue
|
57 |
+
[0.5, 'rgb(206, 192, 250)'], # Purple
|
58 |
+
[1, 'rgb(232, 137, 171)'] # Pink
|
59 |
+
],
|
60 |
+
opacity=0.9,
|
61 |
+
),
|
62 |
+
customdata=df[["label"]],
|
63 |
+
hovertemplate="Dot category: %{customdata[0]}",
|
64 |
+
hoverlabel=dict(namelength=0),
|
65 |
+
showlegend=False
|
66 |
+
))
|
67 |
+
|
68 |
+
|
69 |
+
fig.update_layout(
|
70 |
+
autosize=True,
|
71 |
+
paper_bgcolor='rgba(0,0,0,0)',
|
72 |
+
plot_bgcolor='rgba(0,0,0,0)',
|
73 |
+
showlegend=False,
|
74 |
+
margin=dict(l=0, r=0, t=0, b=0),
|
75 |
+
xaxis=dict(
|
76 |
+
showgrid=False,
|
77 |
+
zeroline=False,
|
78 |
+
showticklabels=False,
|
79 |
+
range=[0, 3]
|
80 |
+
),
|
81 |
+
yaxis=dict(
|
82 |
+
showgrid=False,
|
83 |
+
zeroline=False,
|
84 |
+
showticklabels=False,
|
85 |
+
scaleanchor="x",
|
86 |
+
scaleratio=1,
|
87 |
+
range=[0, 1]
|
88 |
+
)
|
89 |
+
)
|
90 |
+
|
91 |
+
fig.show()
|
92 |
+
|
93 |
+
fig.write_html("../../src/fragments/banner.html",
|
94 |
+
include_plotlyjs=False,
|
95 |
+
full_html=False,
|
96 |
+
config={
|
97 |
+
'displayModeBar': False,
|
98 |
+
'responsive': True,
|
99 |
+
'scrollZoom': False,
|
100 |
+
})
|
python/fragments/bar.py
ADDED
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import plotly.graph_objects as go
|
2 |
+
import plotly.io as pio
|
3 |
+
import os
|
4 |
+
|
5 |
+
"""
|
6 |
+
Simple grouped bar chart (Baseline / Improved / Target), minimal Distill-like style.
|
7 |
+
Responsive, no zoom/pan, clean hover (rounded tooltip corners via post_script).
|
8 |
+
"""
|
9 |
+
|
10 |
+
# Data (five categories)
|
11 |
+
categories = ["A", "B", "C", "D", "E"]
|
12 |
+
baseline = [0.52, 0.61, 0.67, 0.73, 0.78]
|
13 |
+
improved = [0.58, 0.66, 0.72, 0.79, 0.86]
|
14 |
+
target = [0.60, 0.68, 0.75, 0.82, 0.90]
|
15 |
+
|
16 |
+
color_base = "#64748b" # slate-500
|
17 |
+
color_improved = "#2563eb" # blue-600
|
18 |
+
color_target = "#4b5563" # gray-600
|
19 |
+
|
20 |
+
fig = go.Figure()
|
21 |
+
fig.add_bar(
|
22 |
+
x=categories,
|
23 |
+
y=baseline,
|
24 |
+
name="Baseline",
|
25 |
+
marker=dict(color=color_base),
|
26 |
+
offsetgroup="grp",
|
27 |
+
hovertemplate="<b>%{x}</b><br>%{fullData.name}: %{y:.3f}<extra></extra>",
|
28 |
+
)
|
29 |
+
|
30 |
+
fig.add_bar(
|
31 |
+
x=categories,
|
32 |
+
y=improved,
|
33 |
+
name="Improved",
|
34 |
+
marker=dict(color=color_improved),
|
35 |
+
offsetgroup="grp",
|
36 |
+
hovertemplate="<b>%{x}</b><br>%{fullData.name}: %{y:.3f}<extra></extra>",
|
37 |
+
)
|
38 |
+
|
39 |
+
fig.add_bar(
|
40 |
+
x=categories,
|
41 |
+
y=target,
|
42 |
+
name="Target",
|
43 |
+
marker=dict(color=color_target, opacity=0.65, line=dict(color=color_target, width=1)),
|
44 |
+
offsetgroup="grp",
|
45 |
+
hovertemplate="<b>%{x}</b><br>%{fullData.name}: %{y:.3f}<extra></extra>",
|
46 |
+
)
|
47 |
+
|
48 |
+
fig.update_layout(
|
49 |
+
barmode="group",
|
50 |
+
autosize=True,
|
51 |
+
paper_bgcolor="rgba(0,0,0,0)",
|
52 |
+
plot_bgcolor="rgba(0,0,0,0)",
|
53 |
+
margin=dict(l=28, r=12, t=8, b=28),
|
54 |
+
hovermode="x unified",
|
55 |
+
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="left", x=0),
|
56 |
+
xaxis=dict(
|
57 |
+
showgrid=False,
|
58 |
+
zeroline=False,
|
59 |
+
showline=True,
|
60 |
+
linecolor="rgba(0,0,0,0.25)",
|
61 |
+
linewidth=1,
|
62 |
+
ticks="outside",
|
63 |
+
ticklen=6,
|
64 |
+
tickcolor="rgba(0,0,0,0.25)",
|
65 |
+
tickfont=dict(size=12, color="rgba(0,0,0,0.65)"),
|
66 |
+
title=None,
|
67 |
+
automargin=True,
|
68 |
+
fixedrange=True,
|
69 |
+
),
|
70 |
+
yaxis=dict(
|
71 |
+
showgrid=False,
|
72 |
+
zeroline=False,
|
73 |
+
showline=True,
|
74 |
+
linecolor="rgba(0,0,0,0.25)",
|
75 |
+
linewidth=1,
|
76 |
+
ticks="outside",
|
77 |
+
ticklen=6,
|
78 |
+
tickcolor="rgba(0,0,0,0.25)",
|
79 |
+
tickfont=dict(size=12, color="rgba(0,0,0,0.65)"),
|
80 |
+
title=None,
|
81 |
+
tickformat=".2f",
|
82 |
+
automargin=True,
|
83 |
+
fixedrange=True,
|
84 |
+
),
|
85 |
+
)
|
86 |
+
|
87 |
+
post_script = """
|
88 |
+
(function(){
|
89 |
+
var plots = document.querySelectorAll('.js-plotly-plot');
|
90 |
+
plots.forEach(function(gd){
|
91 |
+
function round(){
|
92 |
+
try {
|
93 |
+
var root = gd && gd.parentNode ? gd.parentNode : document;
|
94 |
+
var rects = root.querySelectorAll('.hoverlayer .hovertext rect');
|
95 |
+
rects.forEach(function(r){ r.setAttribute('rx', 8); r.setAttribute('ry', 8); });
|
96 |
+
} catch(e) {}
|
97 |
+
}
|
98 |
+
if (gd && gd.on){
|
99 |
+
gd.on('plotly_hover', round);
|
100 |
+
gd.on('plotly_unhover', round);
|
101 |
+
gd.on('plotly_relayout', round);
|
102 |
+
}
|
103 |
+
setTimeout(round, 0);
|
104 |
+
});
|
105 |
+
})();
|
106 |
+
"""
|
107 |
+
|
108 |
+
html = pio.to_html(
|
109 |
+
fig,
|
110 |
+
include_plotlyjs=False,
|
111 |
+
full_html=False,
|
112 |
+
post_script=post_script,
|
113 |
+
config={
|
114 |
+
"displayModeBar": False,
|
115 |
+
"responsive": True,
|
116 |
+
"scrollZoom": False,
|
117 |
+
"doubleClick": False,
|
118 |
+
"modeBarButtonsToRemove": [
|
119 |
+
"zoom2d", "pan2d", "select2d", "lasso2d",
|
120 |
+
"zoomIn2d", "zoomOut2d", "autoScale2d", "resetScale2d",
|
121 |
+
"toggleSpikelines"
|
122 |
+
],
|
123 |
+
},
|
124 |
+
)
|
125 |
+
|
126 |
+
output_path = os.path.join(os.path.dirname(__file__), "fragments", "bar.html")
|
127 |
+
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
128 |
+
with open(output_path, "w", encoding="utf-8") as f:
|
129 |
+
f.write(html)
|
130 |
+
|
131 |
+
|
python/fragments/heatmap.py
ADDED
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import plotly.graph_objects as go
|
2 |
+
import plotly.io as pio
|
3 |
+
import numpy as np
|
4 |
+
import datetime as dt
|
5 |
+
import os
|
6 |
+
|
7 |
+
"""
|
8 |
+
Calendar-like heatmap (GitHub-style) over the last 52 weeks.
|
9 |
+
Minimal, responsive, transparent background; suitable for Distill.
|
10 |
+
"""
|
11 |
+
|
12 |
+
# Parameters
|
13 |
+
NUM_WEEKS = 52
|
14 |
+
DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
15 |
+
|
16 |
+
# Build dates matrix (7 rows x NUM_WEEKS columns)
|
17 |
+
today = dt.date.today()
|
18 |
+
# Align to start of current week (Monday)
|
19 |
+
start = today - dt.timedelta(days=(today.weekday())) # Monday of current week
|
20 |
+
weeks = [start - dt.timedelta(weeks=w) for w in range(NUM_WEEKS-1, -1, -1)]
|
21 |
+
dates = [[weeks[c] + dt.timedelta(days=r) for c in range(NUM_WEEKS)] for r in range(7)]
|
22 |
+
|
23 |
+
# Generate values (synthetic) — smooth seasonal pattern + noise
|
24 |
+
def gen_value(d: dt.date) -> float:
|
25 |
+
day_of_year = d.timetuple().tm_yday
|
26 |
+
base = 0.5 + 0.45 * np.sin(2 * np.pi * (day_of_year / 365.0))
|
27 |
+
noise = np.random.default_rng(hash(d) % 2**32).uniform(-0.15, 0.15)
|
28 |
+
return max(0.0, min(1.0, base + noise))
|
29 |
+
|
30 |
+
z = [[gen_value(d) for d in row] for row in dates]
|
31 |
+
custom = [[d.isoformat() for d in row] for row in dates]
|
32 |
+
|
33 |
+
# Colors aligned with other charts (slate / blue / gray)
|
34 |
+
colorscale = [
|
35 |
+
[0.00, "#e5e7eb"], # light gray background for low
|
36 |
+
[0.40, "#64748b"], # slate-500
|
37 |
+
[0.75, "#2563eb"], # blue-600
|
38 |
+
[1.00, "#4b5563"], # gray-600 (high end accent)
|
39 |
+
]
|
40 |
+
|
41 |
+
fig = go.Figure(
|
42 |
+
data=go.Heatmap(
|
43 |
+
z=z,
|
44 |
+
x=[w.isoformat() for w in weeks],
|
45 |
+
y=DAYS,
|
46 |
+
colorscale=colorscale,
|
47 |
+
showscale=False,
|
48 |
+
hovertemplate="Date: %{customdata}<br>Value: %{z:.2f}<extra></extra>",
|
49 |
+
customdata=custom,
|
50 |
+
xgap=2,
|
51 |
+
ygap=2,
|
52 |
+
)
|
53 |
+
)
|
54 |
+
|
55 |
+
fig.update_layout(
|
56 |
+
autosize=True,
|
57 |
+
paper_bgcolor="rgba(0,0,0,0)",
|
58 |
+
plot_bgcolor="rgba(0,0,0,0)",
|
59 |
+
margin=dict(l=28, r=12, t=8, b=28),
|
60 |
+
xaxis=dict(
|
61 |
+
showgrid=False,
|
62 |
+
zeroline=False,
|
63 |
+
showline=False,
|
64 |
+
ticks="",
|
65 |
+
showticklabels=False,
|
66 |
+
fixedrange=True,
|
67 |
+
),
|
68 |
+
yaxis=dict(
|
69 |
+
showgrid=False,
|
70 |
+
zeroline=False,
|
71 |
+
showline=False,
|
72 |
+
ticks="",
|
73 |
+
tickfont=dict(size=12, color="rgba(0,0,0,0.65)"),
|
74 |
+
fixedrange=True,
|
75 |
+
),
|
76 |
+
)
|
77 |
+
|
78 |
+
post_script = """
|
79 |
+
(function(){
|
80 |
+
var plots = document.querySelectorAll('.js-plotly-plot');
|
81 |
+
plots.forEach(function(gd){
|
82 |
+
function round(){
|
83 |
+
try {
|
84 |
+
var root = gd && gd.parentNode ? gd.parentNode : document;
|
85 |
+
var rects = root.querySelectorAll('.hoverlayer .hovertext rect');
|
86 |
+
rects.forEach(function(r){ r.setAttribute('rx', 8); r.setAttribute('ry', 8); });
|
87 |
+
} catch(e) {}
|
88 |
+
}
|
89 |
+
if (gd && gd.on){
|
90 |
+
gd.on('plotly_hover', round);
|
91 |
+
gd.on('plotly_unhover', round);
|
92 |
+
gd.on('plotly_relayout', round);
|
93 |
+
}
|
94 |
+
setTimeout(round, 0);
|
95 |
+
});
|
96 |
+
})();
|
97 |
+
"""
|
98 |
+
|
99 |
+
html = pio.to_html(
|
100 |
+
fig,
|
101 |
+
include_plotlyjs=False,
|
102 |
+
full_html=False,
|
103 |
+
post_script=post_script,
|
104 |
+
config={
|
105 |
+
"displayModeBar": False,
|
106 |
+
"responsive": True,
|
107 |
+
"scrollZoom": False,
|
108 |
+
"doubleClick": False,
|
109 |
+
"modeBarButtonsToRemove": [
|
110 |
+
"zoom2d", "pan2d", "select2d", "lasso2d",
|
111 |
+
"zoomIn2d", "zoomOut2d", "autoScale2d", "resetScale2d",
|
112 |
+
"toggleSpikelines"
|
113 |
+
],
|
114 |
+
},
|
115 |
+
)
|
116 |
+
|
117 |
+
output_path = os.path.join(os.path.dirname(__file__), "fragments", "heatmap.html")
|
118 |
+
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
119 |
+
with open(output_path, "w", encoding="utf-8") as f:
|
120 |
+
f.write(html)
|
121 |
+
|
122 |
+
|
python/fragments/line.py
ADDED
@@ -0,0 +1,243 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import plotly.graph_objects as go
|
2 |
+
import plotly.io as pio
|
3 |
+
import numpy as np
|
4 |
+
import os
|
5 |
+
import uuid
|
6 |
+
|
7 |
+
"""
|
8 |
+
Interactive line chart example (3 curves + live slider)
|
9 |
+
|
10 |
+
The slider blends each curve from linear to exponential in real time (no mouseup required).
|
11 |
+
This fragment is safe to insert multiple times on the page (unique IDs per instance).
|
12 |
+
"""
|
13 |
+
|
14 |
+
# Grid (x) and parameterization
|
15 |
+
N = 240
|
16 |
+
x = np.linspace(0, 1, N)
|
17 |
+
|
18 |
+
# Linear baselines (increasing)
|
19 |
+
lin1 = 0.20 + 0.60 * x
|
20 |
+
lin2 = 0.15 + 0.70 * x
|
21 |
+
lin3 = 0.10 + 0.80 * x
|
22 |
+
|
23 |
+
# Helper: normalized exponential on [0,1]
|
24 |
+
def exp_norm(xv: np.ndarray, k: float) -> np.ndarray:
|
25 |
+
return (np.exp(k * xv) - 1.0) / (np.exp(k) - 1.0)
|
26 |
+
|
27 |
+
# Exponential counterparts (similar ranges)
|
28 |
+
exp1 = 0.20 + 0.60 * exp_norm(x, 3.0)
|
29 |
+
exp2 = 0.15 + 0.70 * exp_norm(x, 3.5)
|
30 |
+
exp3 = 0.10 + 0.80 * exp_norm(x, 2.8)
|
31 |
+
|
32 |
+
# Initial blend (alpha=0 ⇒ pure linear)
|
33 |
+
alpha0 = 0.0
|
34 |
+
blend = lambda l, e, a: (1 - a) * l + a * e
|
35 |
+
y1 = blend(lin1, exp1, alpha0)
|
36 |
+
y2 = blend(lin2, exp2, alpha0)
|
37 |
+
y3 = blend(lin3, exp3, alpha0)
|
38 |
+
|
39 |
+
color_base = "#64748b" # slate-500
|
40 |
+
color_improved = "#2563eb" # blue-600
|
41 |
+
color_target = "#4b5563" # gray-600 (dash)
|
42 |
+
|
43 |
+
fig = go.Figure()
|
44 |
+
fig.add_trace(
|
45 |
+
go.Scatter(
|
46 |
+
x=x,
|
47 |
+
y=y1,
|
48 |
+
name="Baseline",
|
49 |
+
mode="lines",
|
50 |
+
line=dict(color=color_base, width=2, shape="spline", smoothing=0.6),
|
51 |
+
hovertemplate="<b>%{fullData.name}</b><br>x=%{x:.2f}<br>y=%{y:.3f}<extra></extra>",
|
52 |
+
showlegend=True,
|
53 |
+
)
|
54 |
+
)
|
55 |
+
fig.add_trace(
|
56 |
+
go.Scatter(
|
57 |
+
x=x,
|
58 |
+
y=y2,
|
59 |
+
name="Improved",
|
60 |
+
mode="lines",
|
61 |
+
line=dict(color=color_improved, width=2, shape="spline", smoothing=0.6),
|
62 |
+
hovertemplate="<b>%{fullData.name}</b><br>x=%{x:.2f}<br>y=%{y:.3f}<extra></extra>",
|
63 |
+
showlegend=True,
|
64 |
+
)
|
65 |
+
)
|
66 |
+
fig.add_trace(
|
67 |
+
go.Scatter(
|
68 |
+
x=x,
|
69 |
+
y=y3,
|
70 |
+
name="Target",
|
71 |
+
mode="lines",
|
72 |
+
line=dict(color=color_target, width=2, dash="dash"),
|
73 |
+
hovertemplate="<b>%{fullData.name}</b><br>x=%{x:.2f}<br>y=%{y:.3f}<extra></extra>",
|
74 |
+
showlegend=True,
|
75 |
+
)
|
76 |
+
)
|
77 |
+
|
78 |
+
fig.update_layout(
|
79 |
+
autosize=True,
|
80 |
+
paper_bgcolor="rgba(0,0,0,0)",
|
81 |
+
plot_bgcolor="rgba(0,0,0,0)",
|
82 |
+
margin=dict(l=28, r=12, t=8, b=28),
|
83 |
+
hovermode="x unified",
|
84 |
+
hoverlabel=dict(
|
85 |
+
bgcolor="white",
|
86 |
+
font=dict(color="#111827", size=12),
|
87 |
+
bordercolor="rgba(0,0,0,0.15)",
|
88 |
+
align="left",
|
89 |
+
namelength=-1,
|
90 |
+
),
|
91 |
+
xaxis=dict(
|
92 |
+
showgrid=False,
|
93 |
+
zeroline=False,
|
94 |
+
showline=True,
|
95 |
+
linecolor="rgba(0,0,0,0.25)",
|
96 |
+
linewidth=1,
|
97 |
+
ticks="outside",
|
98 |
+
ticklen=6,
|
99 |
+
tickcolor="rgba(0,0,0,0.25)",
|
100 |
+
tickfont=dict(size=12, color="rgba(0,0,0,0.55)"),
|
101 |
+
title=None,
|
102 |
+
automargin=True,
|
103 |
+
fixedrange=True,
|
104 |
+
),
|
105 |
+
yaxis=dict(
|
106 |
+
showgrid=False,
|
107 |
+
zeroline=False,
|
108 |
+
showline=True,
|
109 |
+
linecolor="rgba(0,0,0,0.25)",
|
110 |
+
linewidth=1,
|
111 |
+
ticks="outside",
|
112 |
+
ticklen=6,
|
113 |
+
tickcolor="rgba(0,0,0,0.25)",
|
114 |
+
tickfont=dict(size=12, color="rgba(0,0,0,0.55)"),
|
115 |
+
title=None,
|
116 |
+
tickformat=".2f",
|
117 |
+
rangemode="tozero",
|
118 |
+
automargin=True,
|
119 |
+
fixedrange=True,
|
120 |
+
),
|
121 |
+
)
|
122 |
+
|
123 |
+
# Écrit le fragment de manière robuste à côté de ce fichier, dans src/fragments/line.html
|
124 |
+
output_path = os.path.join(os.path.dirname(__file__), "fragments", "line.html")
|
125 |
+
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
126 |
+
|
127 |
+
# Injecte un petit script post-rendu pour arrondir les coins de la boîte de hover
|
128 |
+
post_script = """
|
129 |
+
(function(){
|
130 |
+
function attach(gd){
|
131 |
+
function round(){
|
132 |
+
try {
|
133 |
+
var root = gd && gd.parentNode ? gd.parentNode : document;
|
134 |
+
var rects = root.querySelectorAll('.hoverlayer .hovertext rect');
|
135 |
+
rects.forEach(function(r){ r.setAttribute('rx', 8); r.setAttribute('ry', 8); });
|
136 |
+
} catch(e) {}
|
137 |
+
}
|
138 |
+
if (gd && gd.on) {
|
139 |
+
gd.on('plotly_hover', round);
|
140 |
+
gd.on('plotly_unhover', round);
|
141 |
+
gd.on('plotly_relayout', round);
|
142 |
+
}
|
143 |
+
setTimeout(round, 0);
|
144 |
+
}
|
145 |
+
var plots = document.querySelectorAll('.js-plotly-plot');
|
146 |
+
plots.forEach(attach);
|
147 |
+
})();
|
148 |
+
"""
|
149 |
+
|
150 |
+
html_plot = pio.to_html(
|
151 |
+
fig,
|
152 |
+
include_plotlyjs=False,
|
153 |
+
full_html=False,
|
154 |
+
post_script=post_script,
|
155 |
+
config={
|
156 |
+
"displayModeBar": False,
|
157 |
+
"responsive": True,
|
158 |
+
"scrollZoom": False,
|
159 |
+
"doubleClick": False,
|
160 |
+
"modeBarButtonsToRemove": [
|
161 |
+
"zoom2d", "pan2d", "select2d", "lasso2d",
|
162 |
+
"zoomIn2d", "zoomOut2d", "autoScale2d", "resetScale2d",
|
163 |
+
"toggleSpikelines"
|
164 |
+
],
|
165 |
+
},
|
166 |
+
)
|
167 |
+
|
168 |
+
# Build a self-contained fragment with a live slider (no mouseup required)
|
169 |
+
uid = uuid.uuid4().hex[:8]
|
170 |
+
slider_id = f"line-ex-alpha-{uid}"
|
171 |
+
container_id = f"line-ex-container-{uid}"
|
172 |
+
|
173 |
+
slider_tpl = '''
|
174 |
+
<div id="__CID__">
|
175 |
+
__PLOT__
|
176 |
+
<div class="plotly_controls" style="margin-top:10px; display:flex; gap:14px; align-items:center;">
|
177 |
+
<label style="font-size:12px;color:rgba(0,0,0,.65); display:flex; align-items:center; gap:6px; white-space:nowrap;">
|
178 |
+
Dataset
|
179 |
+
<select id="__DSID__" style="font-size:12px; padding:2px 6px;">
|
180 |
+
<option value="0">Dataset A</option>
|
181 |
+
<option value="1">Dataset B</option>
|
182 |
+
<option value="2">Dataset C</option>
|
183 |
+
</select>
|
184 |
+
</label>
|
185 |
+
<label style="font-size:12px;color:rgba(0,0,0,.65);display:flex;align-items:center;gap:8px; flex:1;">
|
186 |
+
Nonlinearity
|
187 |
+
<input id="__SID__" type="range" min="0" max="1" step="0.01" value="__A0__" style="flex:1;">
|
188 |
+
<span class="alpha-value">__A0__</span>
|
189 |
+
</label>
|
190 |
+
</div>
|
191 |
+
</div>
|
192 |
+
<script>
|
193 |
+
(function(){
|
194 |
+
var container = document.getElementById('__CID__');
|
195 |
+
if(!container) return;
|
196 |
+
var gd = container.querySelector('.js-plotly-plot');
|
197 |
+
var slider = document.getElementById('__SID__');
|
198 |
+
var dsSelect = document.getElementById('__DSID__');
|
199 |
+
var valueEl = container.querySelector('.alpha-value');
|
200 |
+
var N = __N__;
|
201 |
+
var xs = Array.from({length: N}, function(_,i){ return i/(N-1); });
|
202 |
+
function expNorm(x,k){ return (Math.exp(k*x)-1)/(Math.exp(k)-1); }
|
203 |
+
function blend(l,e,a){ return (1-a)*l + a*e; }
|
204 |
+
var datasets = [
|
205 |
+
{ curves: [ {o:0.20,s:0.60,k:3.0}, {o:0.15,s:0.70,k:3.5}, {o:0.10,s:0.80,k:2.8} ] },
|
206 |
+
{ curves: [ {o:0.30,s:0.55,k:2.2}, {o:0.18,s:0.65,k:2.8}, {o:0.12,s:0.70,k:2.0} ] },
|
207 |
+
{ curves: [ {o:0.10,s:0.85,k:3.8}, {o:0.12,s:0.80,k:3.2}, {o:0.08,s:0.90,k:3.0} ] }
|
208 |
+
];
|
209 |
+
var dsi = 0;
|
210 |
+
function makeY(a){
|
211 |
+
var cs = datasets[dsi].curves;
|
212 |
+
var y1 = xs.map(function(x){ return blend(cs[0].o + cs[0].s*x, cs[0].o + cs[0].s*expNorm(x,cs[0].k), a); });
|
213 |
+
var y2 = xs.map(function(x){ return blend(cs[1].o + cs[1].s*x, cs[1].o + cs[1].s*expNorm(x,cs[1].k), a); });
|
214 |
+
var y3 = xs.map(function(x){ return blend(cs[2].o + cs[2].s*x, cs[2].o + cs[2].s*expNorm(x,cs[2].k), a); });
|
215 |
+
return [y1,y2,y3];
|
216 |
+
}
|
217 |
+
function apply(a){
|
218 |
+
var ys = makeY(a);
|
219 |
+
Plotly.restyle(gd, {y:[ys[0]]}, [0]);
|
220 |
+
Plotly.restyle(gd, {y:[ys[1]]}, [1]);
|
221 |
+
Plotly.restyle(gd, {y:[ys[2]]}, [2]);
|
222 |
+
if(valueEl) valueEl.textContent = a.toFixed(2);
|
223 |
+
}
|
224 |
+
var initA = parseFloat(slider.value)||0;
|
225 |
+
slider.addEventListener('input', function(e){ apply(parseFloat(e.target.value)||0); });
|
226 |
+
dsSelect.addEventListener('change', function(e){ dsi = parseInt(e.target.value)||0; apply(parseFloat(slider.value)||0); });
|
227 |
+
setTimeout(function(){ apply(initA); }, 0);
|
228 |
+
})();
|
229 |
+
</script>
|
230 |
+
'''
|
231 |
+
|
232 |
+
slider_html = (slider_tpl
|
233 |
+
.replace('__CID__', container_id)
|
234 |
+
.replace('__SID__', slider_id)
|
235 |
+
.replace('__A0__', f"{alpha0:.2f}")
|
236 |
+
.replace('__N__', str(N))
|
237 |
+
.replace('__PLOT__', html_plot)
|
238 |
+
)
|
239 |
+
|
240 |
+
with open(output_path, 'w', encoding='utf-8') as f:
|
241 |
+
f.write(slider_html)
|
242 |
+
|
243 |
+
|