Skip to content

Commit 2ac2664

Browse files
committed
feat: use MkDocs to generate documentation from plot-schema.json
1. Copy `make_ref_pages.py` from graphing-library-docs repository to create `bin/make_ref_pages.py` and then refactor to generate Markdown stubs for documentation pages. 1. Write `bin/gen.py` to generate HTML pages from the Markdown stubs using the Jinja templates in the `theme` directory for debugging. 1. Convert Jekyll templates from graphing-library-docs repository to Jinja and store in `theme` directory. 1. Write `requirements.txt` file to install required Python packages. - `beautifulsoup4`: for HTML manipulation - `html5validator`: to validate generated HTML - `jinja2`: for manual template expansion - `mkdocs`: for website generation - `mkdocs-exclude`: to exclude `.jinja` files from generation - `python-frontmatter`: for manual template expansion - `ruff`: for checking `bin` scripts 1. Write `Makefile` to automate build and test. 1. Modify documentation comments in some JavaScript files to remove stray backticks. Notes: 1. As of the time of this commit, the `mkdocs_data_loader` plugin must be installed directly from a `.whl` file or similar. It will be added to `requirements.txt` once the plugin is on PyPI. 1. Jinja does not expand directives in Markdown files; it only expands those it finds in template `.jinja` files. Because of this, the Markdown stubs generated by `bin/make_ref_pages.py` have YAML headers but no bodies. 1. The logic to set the `details` variable in `theme/main.jinja` is copied from the original Jekyll templates and modified by trial and error to produce (what appears to be) the right answer. If pages contain extra information, or if information is missing, the most likely cause is an error here. 1. At the time of this commit, no styling is applied to the generated pages and they do not contain headers, footers, or navigation. All of this should be added once a general site theme is developed. 1. `bin/gen.py` defines a `backtick` filter to convert backtick'd pieces of text to `<code>` blocks. This is currently *not* used in the Jinja templates because (a) the existing documentation includes the backticks as-is and (b) I couldn't be bothered to figure out how to add it to MkDocs.
1 parent 3cafc65 commit 2ac2664

17 files changed

+620
-4
lines changed

.gitignore

+5
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,8 @@ tags
1616
!.circleci
1717
!.gitignore
1818
!.npmignore
19+
20+
docs
21+
ref_pages
22+
__pycache__
23+
tmp

Makefile

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
all: commands
2+
3+
BUNDLE=build/plotly.js
4+
OUT_DIR=docs
5+
SCHEMA=test/plot-schema.json
6+
REF_PAGES=ref_pages
7+
THEME=theme
8+
9+
## site: rebuild with MkDocs
10+
site:
11+
mkdocs build
12+
13+
## pages: make all the pages
14+
pages:
15+
python bin/gen.py \
16+
--out docs \
17+
--schema ${SCHEMA} \
18+
--stubs ${REF_PAGES} \
19+
--theme ${THEME}
20+
21+
## figure: generate docs for a figure (annotations.html)
22+
figure:
23+
python bin/gen.py \
24+
--crash \
25+
--out docs \
26+
--schema ${SCHEMA} \
27+
--stubs ${REF_PAGES} \
28+
--theme ${THEME} \
29+
annotations.md
30+
31+
## global: generate docs for global (global.html)
32+
global:
33+
python bin/gen.py \
34+
--crash \
35+
--out docs \
36+
--schema ${SCHEMA} \
37+
--stubs ${REF_PAGES} \
38+
--theme ${THEME} \
39+
global.md
40+
41+
## subplot: generate docs for a subplot (polar.html)
42+
subplot:
43+
python bin/gen.py \
44+
--crash \
45+
--out docs \
46+
--schema ${SCHEMA} \
47+
--stubs ${REF_PAGES} \
48+
--theme ${THEME} \
49+
polar.md
50+
51+
## trace: generate docs for a trace (violin.html)
52+
trace:
53+
python bin/gen.py \
54+
--crash \
55+
--out docs \
56+
--schema ${SCHEMA} \
57+
--stubs ${REF_PAGES} \
58+
--theme ${THEME} \
59+
violin.md
60+
61+
## stubs: make reference page stubs
62+
stubs: ${SCHEMA}
63+
python bin/make_ref_pages.py \
64+
--pages ${REF_PAGES} \
65+
--schema ${SCHEMA} \
66+
--verbose
67+
68+
## validate: check the generated HTML
69+
validate:
70+
@html5validator --root docs
71+
72+
## regenerate JavaScript schema
73+
schema:
74+
npm run schema dist
75+
76+
## -------- : --------
77+
## commands: show available commands
78+
# Note: everything with a leading double '##' and a colon is shown.
79+
commands:
80+
@grep -h -E '^##' ${MAKEFILE_LIST} \
81+
| sed -e 's/## //g' \
82+
| column -t -s ':'
83+
84+
## find-subplots: use jq to find subplot objects
85+
find-subplots:
86+
@cat tmp/plot-schema-formatted.json | jq 'paths | select(.[length-1] == "_isSubplotObj")'
87+
88+
## lint: check code and project
89+
lint:
90+
@ruff check bin
91+
92+
## clean: erase all generated content
93+
clean:
94+
@find . -name '*~' -exec rm {} \;
95+
@rm -rf ${REF_PAGES} ${OUT_DIR}

bin/gen.py

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""Build plotly.js documentation using jinja template."""
2+
3+
import argparse
4+
import frontmatter
5+
from jinja2 import Environment, FileSystemLoader
6+
from jinja2.exceptions import TemplateError
7+
import json
8+
import markdown
9+
from pathlib import Path
10+
import sys
11+
12+
import plugins
13+
14+
15+
KEYS_TO_IGNORE = {
16+
"_isSubplotObj",
17+
"editType",
18+
"role",
19+
}
20+
SUBPLOT = "_isSubplotObj"
21+
22+
23+
def main():
24+
"""Main driver."""
25+
opt = parse_args()
26+
schema = json.loads(Path(opt.schema).read_text())
27+
env = Environment(loader=FileSystemLoader(opt.theme))
28+
env.filters["backtick"] = plugins.backtick
29+
env.filters["debug"] = plugins.debug
30+
all_pages = opt.page if opt.page else [p.name for p in Path(opt.stubs).glob("*.md")]
31+
err_count = 0
32+
for page in all_pages:
33+
if opt.crash:
34+
render_page(opt, schema, env, page)
35+
else:
36+
try:
37+
render_page(opt, schema, env, page)
38+
except Exception as exc:
39+
print(f"ERROR in {page}: {exc}", file=sys.stderr)
40+
err_count += 1
41+
print(f"ERRORS: {err_count} / {len(all_pages)}")
42+
43+
44+
def get_details(schema, page):
45+
"""Temporary hack to pull details out of schema and page header."""
46+
# Trace
47+
if "full_name" not in page:
48+
return page
49+
50+
key = page["name"].split(".")[-1]
51+
entry = schema["layout"]["layoutAttributes"][key]
52+
53+
# Subplot
54+
if SUBPLOT in entry:
55+
return entry
56+
57+
# Figure
58+
return list(entry["items"].values())[0]
59+
60+
61+
def parse_args():
62+
"""Parse command-line arguments."""
63+
parser = argparse.ArgumentParser()
64+
parser.add_argument("--crash", action="store_true", help="crash on first error")
65+
parser.add_argument("--out", help="name of output directory")
66+
parser.add_argument("--schema", required=True, help="path to schema JSON")
67+
parser.add_argument("--stubs", required=True, help="path to stubs directory")
68+
parser.add_argument("--theme", required=True, help="path to theme directory")
69+
parser.add_argument("page", nargs="...", help="name(s) of source file in stubs directory")
70+
return parser.parse_args()
71+
72+
73+
def render_page(opt, schema, env, page_name):
74+
"""Render a single page."""
75+
stem = Path(page_name).stem
76+
loaded = frontmatter.load(Path(opt.stubs, page_name))
77+
metadata = loaded.metadata
78+
assert "template" in metadata, f"page {page_name} does not specify 'template'"
79+
content = loaded.content
80+
details = get_details(schema, metadata)
81+
template = env.get_template(metadata["template"])
82+
html = template.render(
83+
page={"title": stem, "meta": metadata},
84+
config={"data": {"plot-schema": schema}},
85+
details=details,
86+
keys_to_ignore=KEYS_TO_IGNORE,
87+
content=content,
88+
)
89+
if opt.out:
90+
Path(opt.out, stem).mkdir(parents=True, exist_ok=True)
91+
Path(opt.out, stem, "index.html").write_text(html)
92+
else:
93+
print(html)
94+
95+
96+
if __name__ == "__main__":
97+
main()

bin/make_ref_pages.py

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
"""Generate API reference pages from JSON metadata extracted from JavaScript source."""
2+
3+
import argparse
4+
import json
5+
from pathlib import Path
6+
import sys
7+
8+
9+
# Suffix for generated files.
10+
SUFFIX = "md"
11+
12+
# Attributes to document.
13+
ATTRIBUTES = [
14+
"annotations",
15+
"coloraxis",
16+
"geo",
17+
"images",
18+
"map",
19+
"mapbox",
20+
"polar",
21+
"scene",
22+
"selections",
23+
"shapes",
24+
"sliders",
25+
"smith",
26+
"ternary",
27+
"updatemenus",
28+
"xaxis",
29+
"yaxis",
30+
]
31+
32+
# Template for documentation of attributes.
33+
ATTRIBUTE_TEMPLATE = """---
34+
template: attribute.jinja
35+
permalink: /javascript/reference/{full_attribute_path}/
36+
name: {attribute}
37+
full_name: {full_attribute}
38+
description: Figure attribute reference for Plotly's JavaScript open-source graphing library.
39+
parentlink: layout
40+
block: layout
41+
parentpath: layout
42+
---
43+
"""
44+
45+
# Documenting top-level layout.
46+
GLOBAL_PAGE = """---
47+
template: global.jinja
48+
permalink: /javascript/reference/layout/
49+
name: layout
50+
description: Figure attribute reference for Plotly's JavaScript open-source graphing library.
51+
parentlink: layout
52+
block: layout
53+
parentpath: layout
54+
mustmatch: global
55+
---
56+
"""
57+
58+
# Template for documentation of trace.
59+
TRACE_TEMPLATE = """---
60+
template: trace.jinja
61+
permalink: /javascript/reference/{trace}/
62+
trace: {trace}
63+
description: Figure attribute reference for Plotly's JavaScript open-source graphing library.
64+
---
65+
"""
66+
67+
68+
def main():
69+
"""Main driver."""
70+
try:
71+
opt = parse_args()
72+
schema = json.loads(Path(opt.schema).read_text())
73+
make_global(opt)
74+
for attribute in ATTRIBUTES:
75+
make_attribute(opt, attribute)
76+
for trace in schema["traces"]:
77+
make_trace(opt, trace)
78+
except AssertionError as exc:
79+
print(str(exc), file=sys.stderr)
80+
sys.exit(1)
81+
82+
83+
def make_attribute(opt, attribute):
84+
"""Write reference pages for attributes."""
85+
full_attribute = f"layout.{attribute}"
86+
content = ATTRIBUTE_TEMPLATE.format(
87+
full_attribute=full_attribute,
88+
full_attribute_path=full_attribute.replace(".", "/"),
89+
attribute=attribute,
90+
)
91+
write_page(opt, "attribute", f"{attribute}.{SUFFIX}", content)
92+
93+
94+
def make_global(opt):
95+
"""Make top-level 'global' page."""
96+
write_page(opt, "global", f"global.{SUFFIX}", GLOBAL_PAGE)
97+
98+
99+
100+
def make_trace(opt, trace):
101+
"""Write reference page for trace."""
102+
content = TRACE_TEMPLATE.format(trace=trace,)
103+
write_page(opt, "trace", f"{trace}.{SUFFIX}", content)
104+
105+
106+
def parse_args():
107+
"""Parse command-line arguments."""
108+
parser = argparse.ArgumentParser()
109+
parser.add_argument("--pages", required=True, help="where to write generated page stubs")
110+
parser.add_argument("--schema", required=True, help="path to plot schema file")
111+
parser.add_argument("--verbose", action="store_true", help="report progress")
112+
return parser.parse_args()
113+
114+
115+
def write_page(opt, kind, page_name, content):
116+
"""Save a page."""
117+
output_path = Path(f"{opt.pages}/{page_name}")
118+
output_path.parent.mkdir(parents=True, exist_ok=True)
119+
output_path.write_text(content)
120+
if opt.verbose:
121+
print(f"{kind}: {output_path}", file=sys.stderr)
122+
123+
124+
if __name__ == "__main__":
125+
main()

bin/plugins.py

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"""Jinja plugins."""
2+
3+
import re
4+
import sys
5+
6+
7+
BACKTICK_RE = re.compile(r'`(.+?)`')
8+
def backtick(text):
9+
"""Regex replacement reordered."""
10+
return BACKTICK_RE.sub(r"<code>\1</code>", text)
11+
12+
13+
def debug(msg):
14+
"""Print debugging message during template expansion."""
15+
print(msg, file=sys.stderr)
16+
17+
18+
# If being loaded by MkDocs, register the filters.
19+
if "define_env" in globals():
20+
def define_env(env):
21+
env.filters["backtick"] = backtick
22+
env.filters["debug"] = debug

bin/pretty-html.py

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Prettify HTML."""
2+
3+
from bs4 import BeautifulSoup
4+
import sys
5+
6+
sys.stdout.write(BeautifulSoup(sys.stdin.read(), "html.parser").prettify())

mkdocs.yml

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
site_name: Plotly Libraries
2+
site_description: Documentation for Plotly libraries
3+
site_url: https://example.com
4+
copyright: Plotly Inc.
5+
6+
docs_dir: ref_pages
7+
site_dir: docs
8+
theme:
9+
name: null
10+
custom_dir: theme
11+
12+
plugins:
13+
- exclude:
14+
regex:
15+
- '.*\.jinja'
16+
- mkdocs-data-loader:
17+
dir: dist
18+
key: data

requirements.txt

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
beautifulsoup4
2+
html5validator
3+
jinja2
4+
mkdocs
5+
mkdocs-exclude
6+
python-frontmatter
7+
ruff

src/components/errorbars/attributes.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ module.exports = {
1616
description: [
1717
'Determines the rule used to generate the error bars.',
1818

19-
'If *constant`, the bar lengths are of a constant value.',
19+
'If *constant*, the bar lengths are of a constant value.',
2020
'Set this constant in `value`.',
2121

2222
'If *percent*, the bar lengths correspond to a percentage of',

0 commit comments

Comments
 (0)