|
import hashlib |
|
import os |
|
import json |
|
import random |
|
import collections |
|
from operator import itemgetter |
|
import warnings |
|
|
|
import jinja2 |
|
|
|
from docutils import nodes |
|
from docutils.statemachine import ViewList |
|
from docutils.parsers.rst import Directive |
|
from docutils.parsers.rst.directives import flag |
|
|
|
from sphinx.util.nodes import nested_parse_with_titles |
|
|
|
from .utils import ( |
|
get_docstring_and_rest, |
|
prev_this_next, |
|
create_thumbnail, |
|
create_generic_image, |
|
) |
|
from altair.utils.execeval import eval_block |
|
from altair.examples import iter_examples |
|
|
|
|
|
EXAMPLE_MODULE = "altair.examples" |
|
|
|
|
|
GALLERY_TEMPLATE = jinja2.Template( |
|
""" |
|
.. This document is auto-generated by the altair-gallery extension. Do not modify directly. |
|
|
|
.. _{{ gallery_ref }}: |
|
|
|
{{ title }} |
|
{% for char in title %}-{% endfor %} |
|
|
|
This gallery contains a selection of examples of the plots Altair can create. |
|
|
|
Some may seem fairly complicated at first glance, but they are built by combining a simple set of declarative building blocks. |
|
|
|
Many draw upon sample datasets compiled by the `Vega <https://vega.github.io/vega/>`_ project. To access them yourself, install `vega_datasets <https://github.com/altair-viz/vega_datasets>`_. |
|
|
|
.. code-block:: none |
|
|
|
$ pip install vega_datasets |
|
|
|
{% for grouper, group in examples %} |
|
|
|
.. _gallery-category-{{ grouper }}: |
|
|
|
{{ grouper }} |
|
{% for char in grouper %}~{% endfor %} |
|
|
|
.. raw:: html |
|
|
|
<span class="gallery"> |
|
{% for example in group %} |
|
<a class="imagegroup" href="{{ example.name }}.html"> |
|
<span class="image" alt="{{ example.title }}" style="background-image: url({{ image_dir }}/{{ example.name }}-thumb.png);"></span> |
|
<span class="image-title">{{ example.title }}</span> |
|
</a> |
|
{% endfor %} |
|
</span> |
|
|
|
<div style='clear:both;'></div> |
|
|
|
.. toctree:: |
|
:hidden: |
|
{% for example in group %} |
|
{{ example.name }} |
|
{%- endfor %} |
|
|
|
{% endfor %} |
|
""" |
|
) |
|
|
|
MINIGALLERY_TEMPLATE = jinja2.Template( |
|
""" |
|
.. raw:: html |
|
|
|
<div id="showcase"> |
|
<div class="examples"> |
|
{% for example in examples %} |
|
<a class="preview" href="{{ gallery_dir }}/{{ example.name }}.html" style="background-image: url({{ image_dir }}/{{ example.name }}-thumb.png)"></a> |
|
{% endfor %} |
|
</div> |
|
</div> |
|
""" |
|
) |
|
|
|
|
|
EXAMPLE_TEMPLATE = jinja2.Template( |
|
""" |
|
.. This document is auto-generated by the altair-gallery extension. Do not modify directly. |
|
|
|
.. _gallery_{{ name }}: |
|
|
|
{{ docstring }} |
|
|
|
.. altair-plot:: |
|
{% if code_below %}:code-below:{% endif %} |
|
|
|
{{ code | indent(4) }} |
|
|
|
.. toctree:: |
|
:hidden: |
|
""" |
|
) |
|
|
|
|
|
def save_example_pngs(examples, image_dir, make_thumbnails=True): |
|
"""Save example pngs and (optionally) thumbnails""" |
|
if not os.path.exists(image_dir): |
|
os.makedirs(image_dir) |
|
|
|
|
|
hash_file = os.path.join(image_dir, "_image_hashes.json") |
|
|
|
if os.path.exists(hash_file): |
|
with open(hash_file) as f: |
|
hashes = json.load(f) |
|
else: |
|
hashes = {} |
|
|
|
for example in examples: |
|
filename = example["name"] + ".png" |
|
image_file = os.path.join(image_dir, filename) |
|
|
|
example_hash = hashlib.md5(example["code"].encode()).hexdigest() |
|
hashes_match = hashes.get(filename, "") == example_hash |
|
|
|
if hashes_match and os.path.exists(image_file): |
|
print("-> using cached {}".format(image_file)) |
|
else: |
|
|
|
print("-> saving {}".format(image_file)) |
|
chart = eval_block(example["code"]) |
|
try: |
|
chart.save(image_file) |
|
hashes[filename] = example_hash |
|
except ImportError: |
|
warnings.warn("Unable to save image: using generic image") |
|
create_generic_image(image_file) |
|
|
|
with open(hash_file, "w") as f: |
|
json.dump(hashes, f) |
|
|
|
if make_thumbnails: |
|
params = example.get("galleryParameters", {}) |
|
thumb_file = os.path.join(image_dir, example["name"] + "-thumb.png") |
|
create_thumbnail(image_file, thumb_file, **params) |
|
|
|
|
|
with open(hash_file, "w") as f: |
|
json.dump(hashes, f) |
|
|
|
|
|
def populate_examples(**kwds): |
|
"""Iterate through Altair examples and extract code""" |
|
|
|
examples = sorted(iter_examples(), key=itemgetter("name")) |
|
|
|
for example in examples: |
|
docstring, category, code, lineno = get_docstring_and_rest(example["filename"]) |
|
example.update(kwds) |
|
if category is None: |
|
category = "other charts" |
|
example.update( |
|
{ |
|
"docstring": docstring, |
|
"title": docstring.strip().split("\n")[0], |
|
"code": code, |
|
"category": category.title(), |
|
"lineno": lineno, |
|
} |
|
) |
|
|
|
return examples |
|
|
|
|
|
class AltairMiniGalleryDirective(Directive): |
|
has_content = False |
|
|
|
option_spec = { |
|
"size": int, |
|
"names": str, |
|
"indices": lambda x: list(map(int, x.split())), |
|
"shuffle": flag, |
|
"seed": int, |
|
"titles": bool, |
|
"width": str, |
|
} |
|
|
|
def run(self): |
|
size = self.options.get("size", 15) |
|
names = [name.strip() for name in self.options.get("names", "").split(",")] |
|
indices = self.options.get("indices", []) |
|
shuffle = "shuffle" in self.options |
|
seed = self.options.get("seed", 42) |
|
titles = self.options.get("titles", False) |
|
width = self.options.get("width", None) |
|
|
|
env = self.state.document.settings.env |
|
app = env.app |
|
|
|
gallery_dir = app.builder.config.altair_gallery_dir |
|
|
|
examples = populate_examples() |
|
|
|
if names: |
|
if len(names) < size: |
|
raise ValueError( |
|
"altair-minigallery: if names are specified, " |
|
"the list must be at least as long as size." |
|
) |
|
mapping = {example["name"]: example for example in examples} |
|
examples = [mapping[name] for name in names] |
|
else: |
|
if indices: |
|
examples = [examples[i] for i in indices] |
|
if shuffle: |
|
random.seed(seed) |
|
random.shuffle(examples) |
|
if size: |
|
examples = examples[:size] |
|
|
|
include = MINIGALLERY_TEMPLATE.render( |
|
image_dir="/_static", |
|
gallery_dir=gallery_dir, |
|
examples=examples, |
|
titles=titles, |
|
width=width, |
|
) |
|
|
|
|
|
result = ViewList() |
|
for line in include.split("\n"): |
|
result.append(line, "<altair-minigallery>") |
|
node = nodes.paragraph() |
|
node.document = self.state.document |
|
nested_parse_with_titles(self.state, result, node) |
|
|
|
return node.children |
|
|
|
|
|
def main(app): |
|
gallery_dir = app.builder.config.altair_gallery_dir |
|
target_dir = os.path.join(app.builder.srcdir, gallery_dir) |
|
image_dir = os.path.join(app.builder.srcdir, "_images") |
|
|
|
gallery_ref = app.builder.config.altair_gallery_ref |
|
gallery_title = app.builder.config.altair_gallery_title |
|
examples = populate_examples(gallery_ref=gallery_ref, code_below=True) |
|
|
|
if not os.path.exists(target_dir): |
|
os.makedirs(target_dir) |
|
|
|
examples = sorted(examples, key=lambda x: x["title"]) |
|
examples_toc = collections.OrderedDict( |
|
{ |
|
"Simple Charts": [], |
|
"Bar Charts": [], |
|
"Line Charts": [], |
|
"Area Charts": [], |
|
"Circular Plots": [], |
|
"Scatter Plots": [], |
|
"Histograms": [], |
|
"Maps": [], |
|
"Interactive Charts": [], |
|
"Case Studies": [], |
|
"Other Charts": [], |
|
} |
|
) |
|
for d in examples: |
|
examples_toc[d["category"]].append(d) |
|
|
|
|
|
with open(os.path.join(target_dir, "index.rst"), "w") as f: |
|
f.write( |
|
GALLERY_TEMPLATE.render( |
|
title=gallery_title, |
|
examples=examples_toc.items(), |
|
image_dir="/_static", |
|
gallery_ref=gallery_ref, |
|
) |
|
) |
|
|
|
|
|
save_example_pngs(examples, image_dir) |
|
|
|
|
|
for prev_ex, example, next_ex in prev_this_next(examples): |
|
if prev_ex: |
|
example["prev_ref"] = "gallery_{name}".format(**prev_ex) |
|
if next_ex: |
|
example["next_ref"] = "gallery_{name}".format(**next_ex) |
|
target_filename = os.path.join(target_dir, example["name"] + ".rst") |
|
with open(os.path.join(target_filename), "w", encoding="utf-8") as f: |
|
f.write(EXAMPLE_TEMPLATE.render(example)) |
|
|
|
|
|
def setup(app): |
|
app.connect("builder-inited", main) |
|
app.add_css_file("altair-gallery.css") |
|
app.add_config_value("altair_gallery_dir", "gallery", "env") |
|
app.add_config_value("altair_gallery_ref", "example-gallery", "env") |
|
app.add_config_value("altair_gallery_title", "Example Gallery", "env") |
|
app.add_directive_to_domain("py", "altair-minigallery", AltairMiniGalleryDirective) |
|
|