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 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
- <div id="fragment-line"></div>
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
+