Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 21 additions & 6 deletions render.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import subprocess
import base64
import hashlib
import io
from PIL import Image


logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
Expand All @@ -24,7 +26,7 @@
"plotnine": "plotnine",
"lets-plot": "lets-plot",
"plotly": "plotly",
"hvplot": "hvplot (Bokeh)",
"hvplot": "hvPlot (Bokeh)",
"altair": "Altair",
"ggplot": "ggplot2 (R)",
}
Expand All @@ -49,18 +51,29 @@
intro = f.read()


WEBP_QUALITY = 85


def image_from_cell(cell):
"""Save the cell's PNG output as WebP and return its path and pixel size.

The filename stays the MD5 of the base64 PNG, so a plot that hasn't changed
keeps its URL. The pixel size lets the template declare width/height, which
reserves layout space for the lazily loaded images.
"""
try:
for c in cell['outputs']:
if 'data' in c and 'image/png' in c['data']:
base64_img = c['data']['image/png'].replace("\n", "").strip()
filename = hashlib.md5()
filename.update(base64_img.encode('ascii'))
web_path = "/img/plots/{}.png".format(filename.hexdigest())
web_path = "/img/plots/{}.webp".format(filename.hexdigest())
full_path = "web" + web_path
with open(full_path, "wb") as fh:
fh.write(base64.b64decode(base64_img))
return web_path
image = Image.open(io.BytesIO(base64.b64decode(base64_img)))
if image.mode not in ("RGB", "RGBA"):
image = image.convert("RGBA")
image.save(full_path, "WEBP", quality=WEBP_QUALITY, method=6)
return {"path": web_path, "width": image.width, "height": image.height}
except KeyError as e:
logging.error("Can't find image in cell: %s", cell['source'])
raise e
Expand Down Expand Up @@ -145,7 +158,9 @@ def extract_cells(path):
"cell_num": cell_num,
"package": packages.get(tags["package"], tags["package"]),
"package-slug": tags['package'],
"image": image,
"image": image["path"],
"image-width": image["width"],
"image-height": image["height"],
"content": source,
"comment": md.convert(comment) or None,
})
Expand Down
71 changes: 53 additions & 18 deletions templates/t_index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,28 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<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.">
<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.">
<meta name="author" content="Timothy Hopper">

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

<link rel="canonical" href="https://pythonplot.com/">

<meta property="og:title" content="Python Plotting for Exploratory Analysis" />
<meta property="og:type" content="article" />
<meta property="og:url" content="https://pythonplot.com" />
<meta property="og:image" content="http://pythonplot.com/img/cover.png" />
<meta property="og:site_name" content="pythonplot.com" />
<meta property="og:url" content="https://pythonplot.com/" />
<meta property="og:image" content="https://pythonplot.com/img/banner.png" />
<meta property="og:image:alt" content="The same plot drawn with several Python plotting libraries side by side." />
<meta property="og: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." />
<meta name="twitter:card" content="summary" />
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." />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@tdhopper" />
<meta name="twitter:creator" content="@tdhopper" />
<meta name="twitter:title" content="Python Plotting for Exploratory Analysis" />
<meta name="twitter: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." />
<meta name="twitter:image" content="https://pythonplot.com/img/banner.png" />

<link rel="apple-touch-icon" sizes="57x57" href="/img/ico/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/img/ico/apple-icon-60x60.png">
Expand Down Expand Up @@ -74,7 +82,7 @@

<div class="container">

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

Expand All @@ -96,7 +104,7 @@ <h3>Contents</h3>
</div>
</div>

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

Expand All @@ -105,17 +113,17 @@ <h5>{{ name }}</h5>
</div></div>

<hr>
<div class="row mt-4" id="examples"><div class="col-xs-12 col-md-10 offset-md-1">
<div class="row mt-4" id="examples"><div class="col-12 col-md-10 offset-md-1">
{% for (name, slug), items in plots.items() %}
{% set outer_loop = loop %}
<section id="{{ slug }}">
<div class="card mb-4">
<div class="card-header">
<h3 class="card-title mb-1"><a href="#{{ slug }}"><i class="fa fa-link" aria-hidden="true"></i></a> {{ name }}</h3>
<ul class="nav nav-tabs card-header-tabs">
<h3 class="card-title mb-1">{{ name }}<a class="anchor-link" href="#{{ slug }}" title="Link to {{ name }}" aria-label="Link to {{ name }}">#</a></h3>
<ul class="nav nav-pills card-header-pills library-pills">
{% for plot in items %}
<li class="nav-item">
<a class="nav-link {% if loop.index0 == 0 %} active{% endif %}" data-toggle="tab" href="#{{ slug }}{{ plot['package-slug'] }}" role="tab"> {{ plot['package'] }}
</a>
<a class="nav-link{% if loop.index0 == 0 %} active{% endif %}" data-toggle="tab" href="#{{ slug }}{{ plot['package-slug'] }}" role="tab">{{ plot['package'] }}</a>
</li>
{% endfor %}
</ul>
Expand All @@ -124,17 +132,21 @@ <h3 class="card-title mb-1"><a href="#{{ slug }}"><i class="fa fa-link" aria-hid
<div class="card-block p-0">
<div class="tab-content">
{% for plot in items %}
{% set eager = outer_loop.first and loop.first %}
<div class="tab-pane {% if loop.index0 == 0 %} active{% endif %}" id="{{ slug }}{{ plot['package-slug'] }}" role="tabpanel">
<div class="container-fluid">
<div class="row">
<div class="col-lg-6 col-xs-12 p-lg-0">
<div class="col-lg-6 col-12 p-lg-0">
<a href="{{ plot['image'] }}" data-toggle="lightbox" data-title="{{ name }} - {{ plot['package'] }}">
<img src="{{ plot['image'] }}" class="img-fluid">
<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 %}>
</a>
</div>
<div class="col-lg-6 col-xs-12 p-0 right-col d-inline-flex flex-column">
<h6 class="text-muted mb-0 pb-0 pt-3 px-3">Code:</h6>
{% if plot['package'] == "ggplot" %}
<div class="col-lg-6 col-12 p-0 right-col d-inline-flex flex-column">
<div class="code-head d-flex justify-content-between align-items-center pt-3 px-3">
<h6 class="text-muted mb-0 pb-0">Code:</h6>
<button type="button" class="copy-btn" data-copy aria-label="Copy code for {{ name }} with {{ plot['package'] }}">Copy</button>
</div>
{% if plot['package-slug'] == "ggplot" %}
<code class="p-0 mb-auto">{% highlight 'R' %}{{ plot['content'] }}{% endhighlight %}</code>
{% else %}
<code class="p-0 mb-auto">{% highlight 'python' %}{{ plot['content'] }}{% endhighlight %}</code>
Expand Down Expand Up @@ -179,7 +191,30 @@ <h6 class="text-muted">Note:</h6>
event.preventDefault();
$(this).ekkoLightbox({alwaysShowClose: true});
});
$.bigfoot();
</script>

<script type="text/javascript">
document.addEventListener('click', function (event) {
var button = event.target.closest('[data-copy]');
if (!button) { return; }
var panel = button.closest('.right-col');
var code = panel && panel.querySelector('code');
if (!code) { return; }
var text = code.textContent.replace(/\s+$/, '');
var done = function (label) {
button.textContent = label;
setTimeout(function () { button.textContent = 'Copy'; }, 1500);
};
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function () {
done('Copied');
}, function () {
done('Press ⌘C');
});
} else {
done('Press ⌘C');
}
});
</script>
</body>
</html>
77 changes: 77 additions & 0 deletions web/css/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,83 @@ table {
font-family: monospace;
}

/* Subtle deep-link anchor next to each plot heading. */
.anchor-link {
color: #adb5bd;
font-weight: 400;
margin-left: .4rem;
opacity: .45;
text-decoration: none;
}

.anchor-link:hover,
.anchor-link:focus {
color: #495057;
opacity: 1;
text-decoration: none;
}

/* Library selector: compact chips that wrap neatly instead of ragged folder tabs. */
.library-pills {
gap: .25rem;
margin-left: 0;
margin-right: 0;
margin-top: .35rem;
}

.library-pills .nav-item {
margin: 0;
}

.library-pills .nav-link {
background-color: #fff;
border: 1px solid #e0e2e5;
border-radius: 50rem;
color: #495057;
font-size: .85rem;
line-height: 1.3;
padding: .2rem .6rem;
transition: background-color .12s ease, border-color .12s ease, color .12s ease;
white-space: nowrap;
}

.library-pills .nav-link:hover,
.library-pills .nav-link:focus {
background-color: #eceef0;
border-color: #cfd3d7;
color: #212529;
text-decoration: none;
}

.library-pills .nav-link.active,
.library-pills .nav-link.active:hover,
.library-pills .nav-link.active:focus {
background-color: #202020;
border-color: #202020;
color: #fff;
}

/* Copy button sitting in the dark code panel header. */
.copy-btn {
background: transparent;
border: 1px solid #4a4a4a;
border-radius: 3px;
color: #9a9a9a;
cursor: pointer;
font-size: .75rem;
line-height: 1.2;
min-width: 4.5rem;
padding: .15rem .5rem;
transition: color .15s ease, border-color .15s ease;
}

.copy-btn:hover,
.copy-btn:focus {
border-color: #7a7a7a;
color: #e6e6e6;
outline: none;
}

@media (min-width: 1600px) {
.container {
width: 1440px;
Expand Down
Loading