Skip to content

Commit d950ac5

Browse files
authored
Merge pull request #38 from tdhopper/tab-pills
Restyle library selector as pills; fix R highlighting and mobile overflow
2 parents 347f529 + 16995f6 commit d950ac5

3 files changed

Lines changed: 151 additions & 24 deletions

File tree

render.py

Lines changed: 21 additions & 6 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')
@@ -24,7 +26,7 @@
2426
"plotnine": "plotnine",
2527
"lets-plot": "lets-plot",
2628
"plotly": "plotly",
27-
"hvplot": "hvplot (Bokeh)",
29+
"hvplot": "hvPlot (Bokeh)",
2830
"altair": "Altair",
2931
"ggplot": "ggplot2 (R)",
3032
}
@@ -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: 53 additions & 18 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">
@@ -74,7 +82,7 @@
7482

7583
<div class="container">
7684

77-
<div class="row"><div class="col-xs-12 col-lg-6 col-md-8 offset-md-1 offset-lg-2">
85+
<div class="row"><div class="col-12 col-lg-6 col-md-8 offset-md-1 offset-lg-2">
7886
<img class="img-fluid pb-4" src="/img/banner.png">
7987
<h1>Python Plotting for Exploratory Data Analysis</h1>
8088

@@ -96,7 +104,7 @@ <h3>Contents</h3>
96104
</div>
97105
</div>
98106

99-
<div class="row"><div class="col-xs-12 col-lg-8 col-md-8 offset-md-1 offset-lg-2">
107+
<div class="row"><div class="col-12 col-lg-8 col-md-8 offset-md-1 offset-lg-2">
100108
{% for name, table in data.items() %}
101109
<h5>{{ name }}</h5>
102110

@@ -105,17 +113,17 @@ <h5>{{ name }}</h5>
105113
</div></div>
106114

107115
<hr>
108-
<div class="row mt-4" id="examples"><div class="col-xs-12 col-md-10 offset-md-1">
116+
<div class="row mt-4" id="examples"><div class="col-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>
114-
<ul class="nav nav-tabs card-header-tabs">
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>
123+
<ul class="nav nav-pills card-header-pills library-pills">
115124
{% for plot in items %}
116125
<li class="nav-item">
117-
<a class="nav-link {% if loop.index0 == 0 %} active{% endif %}" data-toggle="tab" href="#{{ slug }}{{ plot['package-slug'] }}" role="tab"> {{ plot['package'] }}
118-
</a>
126+
<a class="nav-link{% if loop.index0 == 0 %} active{% endif %}" data-toggle="tab" href="#{{ slug }}{{ plot['package-slug'] }}" role="tab">{{ plot['package'] }}</a>
119127
</li>
120128
{% endfor %}
121129
</ul>
@@ -124,17 +132,21 @@ <h3 class="card-title mb-1"><a href="#{{ slug }}"><i class="fa fa-link" aria-hid
124132
<div class="card-block p-0">
125133
<div class="tab-content">
126134
{% for plot in items %}
135+
{% set eager = outer_loop.first and loop.first %}
127136
<div class="tab-pane {% if loop.index0 == 0 %} active{% endif %}" id="{{ slug }}{{ plot['package-slug'] }}" role="tabpanel">
128137
<div class="container-fluid">
129138
<div class="row">
130-
<div class="col-lg-6 col-xs-12 p-lg-0">
139+
<div class="col-lg-6 col-12 p-lg-0">
131140
<a href="{{ plot['image'] }}" data-toggle="lightbox" data-title="{{ name }} - {{ plot['package'] }}">
132-
<img src="{{ plot['image'] }}" class="img-fluid">
141+
<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 %}>
133142
</a>
134143
</div>
135-
<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>
137-
{% if plot['package'] == "ggplot" %}
144+
<div class="col-lg-6 col-12 p-0 right-col d-inline-flex flex-column">
145+
<div class="code-head d-flex justify-content-between align-items-center pt-3 px-3">
146+
<h6 class="text-muted mb-0 pb-0">Code:</h6>
147+
<button type="button" class="copy-btn" data-copy aria-label="Copy code for {{ name }} with {{ plot['package'] }}">Copy</button>
148+
</div>
149+
{% if plot['package-slug'] == "ggplot" %}
138150
<code class="p-0 mb-auto">{% highlight 'R' %}{{ plot['content'] }}{% endhighlight %}</code>
139151
{% else %}
140152
<code class="p-0 mb-auto">{% highlight 'python' %}{{ plot['content'] }}{% endhighlight %}</code>
@@ -179,7 +191,30 @@ <h6 class="text-muted">Note:</h6>
179191
event.preventDefault();
180192
$(this).ekkoLightbox({alwaysShowClose: true});
181193
});
182-
$.bigfoot();
194+
</script>
195+
196+
<script type="text/javascript">
197+
document.addEventListener('click', function (event) {
198+
var button = event.target.closest('[data-copy]');
199+
if (!button) { return; }
200+
var panel = button.closest('.right-col');
201+
var code = panel && panel.querySelector('code');
202+
if (!code) { return; }
203+
var text = code.textContent.replace(/\s+$/, '');
204+
var done = function (label) {
205+
button.textContent = label;
206+
setTimeout(function () { button.textContent = 'Copy'; }, 1500);
207+
};
208+
if (navigator.clipboard && navigator.clipboard.writeText) {
209+
navigator.clipboard.writeText(text).then(function () {
210+
done('Copied');
211+
}, function () {
212+
done('Press ⌘C');
213+
});
214+
} else {
215+
done('Press ⌘C');
216+
}
217+
});
183218
</script>
184219
</body>
185220
</html>

web/css/custom.css

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,83 @@ 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+
/* Library selector: compact chips that wrap neatly instead of ragged folder tabs. */
47+
.library-pills {
48+
gap: .25rem;
49+
margin-left: 0;
50+
margin-right: 0;
51+
margin-top: .35rem;
52+
}
53+
54+
.library-pills .nav-item {
55+
margin: 0;
56+
}
57+
58+
.library-pills .nav-link {
59+
background-color: #fff;
60+
border: 1px solid #e0e2e5;
61+
border-radius: 50rem;
62+
color: #495057;
63+
font-size: .85rem;
64+
line-height: 1.3;
65+
padding: .2rem .6rem;
66+
transition: background-color .12s ease, border-color .12s ease, color .12s ease;
67+
white-space: nowrap;
68+
}
69+
70+
.library-pills .nav-link:hover,
71+
.library-pills .nav-link:focus {
72+
background-color: #eceef0;
73+
border-color: #cfd3d7;
74+
color: #212529;
75+
text-decoration: none;
76+
}
77+
78+
.library-pills .nav-link.active,
79+
.library-pills .nav-link.active:hover,
80+
.library-pills .nav-link.active:focus {
81+
background-color: #202020;
82+
border-color: #202020;
83+
color: #fff;
84+
}
85+
86+
/* Copy button sitting in the dark code panel header. */
87+
.copy-btn {
88+
background: transparent;
89+
border: 1px solid #4a4a4a;
90+
border-radius: 3px;
91+
color: #9a9a9a;
92+
cursor: pointer;
93+
font-size: .75rem;
94+
line-height: 1.2;
95+
min-width: 4.5rem;
96+
padding: .15rem .5rem;
97+
transition: color .15s ease, border-color .15s ease;
98+
}
99+
100+
.copy-btn:hover,
101+
.copy-btn:focus {
102+
border-color: #7a7a7a;
103+
color: #e6e6e6;
104+
outline: none;
105+
}
106+
30107
@media (min-width: 1600px) {
31108
.container {
32109
width: 1440px;

0 commit comments

Comments
 (0)