Skip to content

Commit 7198f02

Browse files
authored
Merge pull request #29 from tdhopper/perf-ux
2 parents b5168a8 + dc1747c commit 7198f02

3 files changed

Lines changed: 102 additions & 14 deletions

File tree

render.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import subprocess
1212
import base64
1313
import hashlib
14+
import io
15+
from PIL import Image
1416

1517

1618
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
@@ -49,18 +51,29 @@
4951
intro = f.read()
5052

5153

54+
WEBP_QUALITY = 85
55+
56+
5257
def image_from_cell(cell):
58+
"""Save the cell's PNG output as WebP and return its path and pixel size.
59+
60+
The filename stays the MD5 of the base64 PNG, so a plot that hasn't changed
61+
keeps its URL. The pixel size lets the template declare width/height, which
62+
reserves layout space for the lazily loaded images.
63+
"""
5364
try:
5465
for c in cell['outputs']:
5566
if 'data' in c and 'image/png' in c['data']:
5667
base64_img = c['data']['image/png'].replace("\n", "").strip()
5768
filename = hashlib.md5()
5869
filename.update(base64_img.encode('ascii'))
59-
web_path = "/img/plots/{}.png".format(filename.hexdigest())
70+
web_path = "/img/plots/{}.webp".format(filename.hexdigest())
6071
full_path = "web" + web_path
61-
with open(full_path, "wb") as fh:
62-
fh.write(base64.b64decode(base64_img))
63-
return web_path
72+
image = Image.open(io.BytesIO(base64.b64decode(base64_img)))
73+
if image.mode not in ("RGB", "RGBA"):
74+
image = image.convert("RGBA")
75+
image.save(full_path, "WEBP", quality=WEBP_QUALITY, method=6)
76+
return {"path": web_path, "width": image.width, "height": image.height}
6477
except KeyError as e:
6578
logging.error("Can't find image in cell: %s", cell['source'])
6679
raise e
@@ -145,7 +158,9 @@ def extract_cells(path):
145158
"cell_num": cell_num,
146159
"package": packages.get(tags["package"], tags["package"]),
147160
"package-slug": tags['package'],
148-
"image": image,
161+
"image": image["path"],
162+
"image-width": image["width"],
163+
"image-height": image["height"],
149164
"content": source,
150165
"comment": md.convert(comment) or None,
151166
})

templates/t_index.html

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,28 @@
44
<head>
55
<meta charset="utf-8">
66
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
7-
<meta name="description" content="Interactive comparison of Python plotting libraries for exploratory data analysis. Examples of using Pandas plotting, plotnine, Seaborn, and Matplotlib. Includes comparison with ggplot2 for R.">
7+
<meta name="description" content="Side-by-side comparison of Python plotting libraries for exploratory data analysis: pandas, Matplotlib, Seaborn, plotnine, Lets-Plot, plotly, hvPlot, and Altair, with R's ggplot2 for reference.">
88
<meta name="author" content="Timothy Hopper">
99

1010
<title>Python Plotting for Exploratory Analysis</title>
1111

12+
<link rel="canonical" href="https://pythonplot.com/">
13+
1214
<meta property="og:title" content="Python Plotting for Exploratory Analysis" />
1315
<meta property="og:type" content="article" />
14-
<meta property="og:url" content="https://pythonplot.com" />
15-
<meta property="og:image" content="http://pythonplot.com/img/cover.png" />
16+
<meta property="og:site_name" content="pythonplot.com" />
17+
<meta property="og:url" content="https://pythonplot.com/" />
18+
<meta property="og:image" content="https://pythonplot.com/img/banner.png" />
19+
<meta property="og:image:alt" content="The same plot drawn with several Python plotting libraries side by side." />
1620
<meta property="og:description"
17-
content="Interactive comparison of Python plotting libraries for exploratory data analysis. Examples of using Pandas plotting, plotnine, Seaborn, and Matplotlib. Includes comparison with ggplot2 for R." />
18-
<meta name="twitter:card" content="summary" />
21+
content="Side-by-side comparison of Python plotting libraries for exploratory data analysis: pandas, Matplotlib, Seaborn, plotnine, Lets-Plot, plotly, hvPlot, and Altair, with R's ggplot2 for reference." />
22+
<meta name="twitter:card" content="summary_large_image" />
1923
<meta name="twitter:site" content="@tdhopper" />
2024
<meta name="twitter:creator" content="@tdhopper" />
25+
<meta name="twitter:title" content="Python Plotting for Exploratory Analysis" />
26+
<meta name="twitter:description"
27+
content="Side-by-side comparison of Python plotting libraries for exploratory data analysis: pandas, Matplotlib, Seaborn, plotnine, Lets-Plot, plotly, hvPlot, and Altair, with R's ggplot2 for reference." />
28+
<meta name="twitter:image" content="https://pythonplot.com/img/banner.png" />
2129

2230
<link rel="apple-touch-icon" sizes="57x57" href="/img/ico/apple-icon-57x57.png">
2331
<link rel="apple-touch-icon" sizes="60x60" href="/img/ico/apple-icon-60x60.png">
@@ -107,10 +115,11 @@ <h5>{{ name }}</h5>
107115
<hr>
108116
<div class="row mt-4" id="examples"><div class="col-xs-12 col-md-10 offset-md-1">
109117
{% for (name, slug), items in plots.items() %}
118+
{% set outer_loop = loop %}
110119
<section id="{{ slug }}">
111120
<div class="card mb-4">
112121
<div class="card-header">
113-
<h3 class="card-title mb-1"><a href="#{{ slug }}"><i class="fa fa-link" aria-hidden="true"></i></a> {{ name }}</h3>
122+
<h3 class="card-title mb-1">{{ name }}<a class="anchor-link" href="#{{ slug }}" title="Link to {{ name }}" aria-label="Link to {{ name }}">#</a></h3>
114123
<ul class="nav nav-tabs card-header-tabs">
115124
{% for plot in items %}
116125
<li class="nav-item">
@@ -124,16 +133,20 @@ <h3 class="card-title mb-1"><a href="#{{ slug }}"><i class="fa fa-link" aria-hid
124133
<div class="card-block p-0">
125134
<div class="tab-content">
126135
{% for plot in items %}
136+
{% set eager = outer_loop.first and loop.first %}
127137
<div class="tab-pane {% if loop.index0 == 0 %} active{% endif %}" id="{{ slug }}{{ plot['package-slug'] }}" role="tabpanel">
128138
<div class="container-fluid">
129139
<div class="row">
130140
<div class="col-lg-6 col-xs-12 p-lg-0">
131141
<a href="{{ plot['image'] }}" data-toggle="lightbox" data-title="{{ name }} - {{ plot['package'] }}">
132-
<img src="{{ plot['image'] }}" class="img-fluid">
142+
<img src="{{ plot['image'] }}" class="img-fluid" alt="{{ name }} drawn with {{ plot['package'] }}" width="{{ plot['image-width'] }}" height="{{ plot['image-height'] }}" decoding="async"{% if not eager %} loading="lazy"{% endif %}>
133143
</a>
134144
</div>
135145
<div class="col-lg-6 col-xs-12 p-0 right-col d-inline-flex flex-column">
136-
<h6 class="text-muted mb-0 pb-0 pt-3 px-3">Code:</h6>
146+
<div class="code-head d-flex justify-content-between align-items-center pt-3 px-3">
147+
<h6 class="text-muted mb-0 pb-0">Code:</h6>
148+
<button type="button" class="copy-btn" data-copy aria-label="Copy code for {{ name }} with {{ plot['package'] }}">Copy</button>
149+
</div>
137150
{% if plot['package'] == "ggplot" %}
138151
<code class="p-0 mb-auto">{% highlight 'R' %}{{ plot['content'] }}{% endhighlight %}</code>
139152
{% else %}
@@ -179,7 +192,30 @@ <h6 class="text-muted">Note:</h6>
179192
event.preventDefault();
180193
$(this).ekkoLightbox({alwaysShowClose: true});
181194
});
182-
$.bigfoot();
195+
</script>
196+
197+
<script type="text/javascript">
198+
document.addEventListener('click', function (event) {
199+
var button = event.target.closest('[data-copy]');
200+
if (!button) { return; }
201+
var panel = button.closest('.right-col');
202+
var code = panel && panel.querySelector('code');
203+
if (!code) { return; }
204+
var text = code.textContent.replace(/\s+$/, '');
205+
var done = function (label) {
206+
button.textContent = label;
207+
setTimeout(function () { button.textContent = 'Copy'; }, 1500);
208+
};
209+
if (navigator.clipboard && navigator.clipboard.writeText) {
210+
navigator.clipboard.writeText(text).then(function () {
211+
done('Copied');
212+
}, function () {
213+
done('Press ⌘C');
214+
});
215+
} else {
216+
done('Press ⌘C');
217+
}
218+
});
183219
</script>
184220
</body>
185221
</html>

web/css/custom.css

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,43 @@ table {
2727
font-family: monospace;
2828
}
2929

30+
/* Subtle deep-link anchor next to each plot heading. */
31+
.anchor-link {
32+
color: #adb5bd;
33+
font-weight: 400;
34+
margin-left: .4rem;
35+
opacity: .45;
36+
text-decoration: none;
37+
}
38+
39+
.anchor-link:hover,
40+
.anchor-link:focus {
41+
color: #495057;
42+
opacity: 1;
43+
text-decoration: none;
44+
}
45+
46+
/* Copy button sitting in the dark code panel header. */
47+
.copy-btn {
48+
background: transparent;
49+
border: 1px solid #4a4a4a;
50+
border-radius: 3px;
51+
color: #9a9a9a;
52+
cursor: pointer;
53+
font-size: .75rem;
54+
line-height: 1.2;
55+
min-width: 4.5rem;
56+
padding: .15rem .5rem;
57+
transition: color .15s ease, border-color .15s ease;
58+
}
59+
60+
.copy-btn:hover,
61+
.copy-btn:focus {
62+
border-color: #7a7a7a;
63+
color: #e6e6e6;
64+
outline: none;
65+
}
66+
3067
@media (min-width: 1600px) {
3168
.container {
3269
width: 1440px;

0 commit comments

Comments
 (0)