-
-
Notifications
You must be signed in to change notification settings - Fork 830
docs: Adds Vega-Altair Theme Test #3630
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
40 commits
Select commit
Hold shift + click to select a range
db3557d
docs: Adds `altair_theme_test.py`
dangotbanned bc60d23
chore: Vendor `alt.utils.html.STANDARD` template
dangotbanned ffc8345
refactor: Remove static conditionals in template
dangotbanned 4b4d4fd
feat: Add theme select input
dangotbanned bad2bcf
chore: Add temporary `render_write` helper
dangotbanned f7aeef8
refactor: Move chart concat into function
dangotbanned 0fae2ab
feat(DRAFT): Adapt more of `index.html` script
dangotbanned f6971de
fix: Pass theme name to correct property
dangotbanned d59ce76
docs: Ensure all charts have tooltips
dangotbanned 23a2f91
docs: Adds static `vega-altair_theme_test.html`
dangotbanned 8c6544c
ci: Temp pin `mypy`
dangotbanned 0b07f27
revert: Remove temp `mypy` pin
dangotbanned 3e72fe1
Merge branch 'main' into altair-theme-test
dangotbanned d1ea691
docs: Link to `vega-altair_theme_test.html` in `#chart-themes`
dangotbanned 436a65d
refactor: Remove `polars` dependency
dangotbanned f466d8a
refactor: Move entire chart definition to `alt_theme_test`
dangotbanned 85ba0b6
refactor: Move imports inline
dangotbanned 1773498
feat: Adds `tools.codemod.py`
dangotbanned 1945d2b
feat: Adds `.. altair-code-ref::` directive
dangotbanned 8221d6c
docs: Add folding code block for `Vega-Altair Theme Test`
dangotbanned db14314
fix(typing): Remove unused type ignore
dangotbanned aa00dc1
fix(typing): Add patch for `vl-convert-python=1.7.0`
dangotbanned 712c646
Merge remote-tracking branch 'upstream/main' into altair-theme-test
dangotbanned 6e1ca94
refactor: Render html after `generate_api_docs`
dangotbanned 3a5801e
style: Trim some whitespace
dangotbanned 42491c7
Merge branch 'main' into altair-theme-test
dangotbanned b04dc02
Merge branch 'main' into altair-theme-test
dangotbanned 6568cdf
refactor: Factor out `vega_datasets` dependency
dangotbanned 85a2a4f
fix: Run `generate-schema-wrapper`
dangotbanned 2fe68f8
fix(typing): Add type ignore
dangotbanned 0694233
feat(DRAFT): Adds Functional `altair-pyscript` directive
dangotbanned 6803368
revert: Remove `generate_static_docs`
dangotbanned 99400d5
refactor: Rewrite as `altair-theme`
dangotbanned cbdf854
feat: Support `:summary:` in `altair-theme`
dangotbanned 7cf627d
docs: Add warning for `ast.unparse` use
dangotbanned 0615228
refactor: Adds `tools.codemod.Ruff`
dangotbanned 567796e
docs: Redesign to fit User Guide template
dangotbanned c82ecbf
revert: Remove `THEME_TEST_TEMPLATE`, `render_theme_test`
dangotbanned 092fe09
docs: Adds *Built-in Themes* section
dangotbanned 1987542
revert: Remove `vega-altair_theme_test.html`
dangotbanned File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,330 @@ | ||
| """Sphinx extension providing formatted code blocks, referencing some function.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import TYPE_CHECKING, Literal, get_args | ||
|
|
||
| from docutils import nodes | ||
| from docutils.parsers.rst import directives | ||
| from sphinx.util.docutils import SphinxDirective | ||
| from sphinx.util.parsing import nested_parse_to_nodes | ||
|
|
||
| from altair.vegalite.v5.schema._typing import VegaThemes | ||
| from tools.codemod import extract_func_def, extract_func_def_embed | ||
|
|
||
| if TYPE_CHECKING: | ||
| import sys | ||
| from typing import ( | ||
| Any, | ||
| Callable, | ||
| ClassVar, | ||
| Iterable, | ||
| Iterator, | ||
| Mapping, | ||
| Sequence, | ||
| TypeVar, | ||
| Union, | ||
| ) | ||
|
|
||
| from docutils.parsers.rst.states import RSTState, RSTStateMachine | ||
| from docutils.statemachine import StringList | ||
| from sphinx.application import Sphinx | ||
|
|
||
| if sys.version_info >= (3, 12): | ||
| from typing import TypeAliasType | ||
| else: | ||
| from typing_extensions import TypeAliasType | ||
| if sys.version_info >= (3, 10): | ||
| from typing import TypeAlias | ||
| else: | ||
| from typing_extensions import TypeAlias | ||
|
|
||
| T = TypeVar("T") | ||
| OneOrIter = TypeAliasType("OneOrIter", Union[T, Iterable[T]], type_params=(T,)) | ||
|
|
||
| _OutputShort: TypeAlias = Literal["code", "plot"] | ||
| _OutputLong: TypeAlias = Literal["code-block", "altair-plot"] | ||
| _OUTPUT_REMAP: Mapping[_OutputShort, _OutputLong] = { | ||
| "code": "code-block", | ||
| "plot": "altair-plot", | ||
| } | ||
| _Option: TypeAlias = Literal["output", "fold", "summary"] | ||
|
|
||
| _PYSCRIPT_URL_FMT = "https://pyscript.net/releases/{0}/core.js" | ||
| _PYSCRIPT_VERSION = "2024.10.1" | ||
| _PYSCRIPT_URL = _PYSCRIPT_URL_FMT.format(_PYSCRIPT_VERSION) | ||
|
|
||
|
|
||
| def validate_output(output: Any) -> _OutputLong: | ||
| output = output.strip().lower() | ||
| if output not in {"plot", "code"}: | ||
| msg = f":output: option must be one of {get_args(_OutputShort)!r}" | ||
| raise TypeError(msg) | ||
| else: | ||
| short: _OutputShort = output | ||
| return _OUTPUT_REMAP[short] | ||
|
|
||
|
|
||
| def validate_packages(packages: Any) -> str: | ||
| if packages is None: | ||
| return '["altair"]' | ||
| else: | ||
| split = [pkg.strip() for pkg in packages.split(",")] | ||
| if len(split) == 1: | ||
| return f'["{split[0]}"]' | ||
| else: | ||
| return f'[{",".join(split)}]' | ||
|
|
||
|
|
||
| def raw_html(text: str, /) -> nodes.raw: | ||
| return nodes.raw("", text, format="html") | ||
|
|
||
|
|
||
| def maybe_details( | ||
| parsed: Iterable[nodes.Node], options: dict[_Option, Any], *, default_summary: str | ||
| ) -> Sequence[nodes.Node]: | ||
| """ | ||
| Wrap ``parsed`` in a folding `details`_ block if requested. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| parsed | ||
| Target nodes that have been processed. | ||
| options | ||
| Optional arguments provided to ``.. altair-code-ref::``. | ||
|
|
||
| .. note:: | ||
| If no relevant options are specified, | ||
| ``parsed`` is returned unchanged. | ||
|
|
||
| default_summary | ||
| Label text used when **only** specifying ``:fold:``. | ||
|
|
||
| .. _details: | ||
| https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details | ||
| """ | ||
|
|
||
| def gen() -> Iterator[nodes.Node]: | ||
| if {"fold", "summary"}.isdisjoint(options.keys()): | ||
| yield from parsed | ||
| else: | ||
| summary = options.get("summary", default_summary) | ||
| yield raw_html(f"<p><details><summary><a>{summary}</a></summary>") | ||
| yield from parsed | ||
| yield raw_html("</details></p>") | ||
|
|
||
| return list(gen()) | ||
|
|
||
|
|
||
| def theme_names() -> tuple[Sequence[str], Sequence[str]]: | ||
| names: set[VegaThemes] = set(get_args(VegaThemes)) | ||
| carbon = {nm for nm in names if nm.startswith("carbon")} | ||
| return ["default", *sorted(names - carbon)], sorted(carbon) | ||
|
|
||
|
|
||
| def option(label: str, value: str | None = None, /) -> nodes.raw: | ||
| s = f"<option value={value!r}>" if value else "<option>" | ||
| return raw_html(f"{s}{label}</option>\n") | ||
|
|
||
|
|
||
| def optgroup(label: str, *options: OneOrIter[nodes.raw]) -> Iterator[nodes.raw]: | ||
| yield raw_html(f"<optgroup label={label!r}>\n") | ||
| for opt in options: | ||
| if isinstance(opt, nodes.raw): | ||
| yield opt | ||
| else: | ||
| yield from opt | ||
| yield raw_html("</optgroup>\n") | ||
|
|
||
|
|
||
| def dropdown( | ||
| id: str, label: str | None, extra_select: str, *options: OneOrIter[nodes.raw] | ||
| ) -> Iterator[nodes.raw]: | ||
| if label: | ||
| yield raw_html(f"<label for={id!r}>{label}</label>\n") | ||
| select_text = f"<select id={id!r}" | ||
| if extra_select: | ||
| select_text = f"{select_text} {extra_select}" | ||
| yield raw_html(f"{select_text}>\n") | ||
| for opt in options: | ||
| if isinstance(opt, nodes.raw): | ||
| yield opt | ||
| else: | ||
| yield from opt | ||
| yield raw_html("</select>\n") | ||
|
|
||
|
|
||
| def pyscript( | ||
| packages: str, target_div_id: str, loading_label: str, py_code: str | ||
| ) -> Iterator[nodes.raw]: | ||
| PY = "py" | ||
| LB, RB = "{", "}" | ||
| packages = f""""packages":{packages}""" | ||
| yield raw_html(f"<div id={target_div_id!r}>{loading_label}</div>\n") | ||
| yield raw_html(f"<script type={PY!r} config='{LB}{packages}{RB}'>\n") | ||
| yield raw_html(py_code) | ||
| yield raw_html("</script>\n") | ||
|
|
||
|
|
||
| def _before_code(refresh_name: str, select_id: str, target_div_id: str) -> str: | ||
| INDENT = " " * 4 | ||
| return ( | ||
| f"from js import document\n" | ||
| f"from pyscript import display\n" | ||
| f"import altair as alt\n\n" | ||
| f"def {refresh_name}(*args):\n" | ||
| f"{INDENT}selected = document.getElementById({select_id!r}).value\n" | ||
| f"{INDENT}alt.renderers.set_embed_options(theme=selected)\n" | ||
| f"{INDENT}display(chart, append=False, target={target_div_id!r})\n" | ||
| ) | ||
|
|
||
|
|
||
| class ThemeDirective(SphinxDirective): | ||
| """ | ||
| Theme preview directive. | ||
|
|
||
| Similar to ``CodeRefDirective``, but uses `PyScript`_ to access the browser. | ||
|
|
||
| .. _PyScript: | ||
| https://pyscript.net/ | ||
| """ | ||
|
|
||
| has_content: ClassVar[Literal[False]] = False | ||
| required_arguments: ClassVar[Literal[1]] = 1 | ||
| option_spec = { | ||
| "packages": validate_packages, | ||
| "dropdown-label": directives.unchanged, | ||
| "loading-label": directives.unchanged, | ||
| "fold": directives.flag, | ||
| "summary": directives.unchanged_required, | ||
| } | ||
|
|
||
| def run(self) -> Sequence[nodes.Node]: | ||
| results: list[nodes.Node] = [] | ||
| SELECT_ID = "embed_theme" | ||
| REFRESH_NAME = "apply_embed_input" | ||
| TARGET_DIV_ID = "render_altair" | ||
| standard_names, carbon_names = theme_names() | ||
|
|
||
| qual_name = self.arguments[0] | ||
| module_name, func_name = qual_name.rsplit(".", 1) | ||
| dropdown_label = self.options.get("dropdown-label", "Select theme:") | ||
| loading_label = self.options.get("loading-label", "loading...") | ||
| packages: str = self.options.get("packages", validate_packages(None)) | ||
|
|
||
| results.append(raw_html("<div><p>\n")) | ||
| results.extend( | ||
| dropdown( | ||
| SELECT_ID, | ||
| dropdown_label, | ||
| f"py-input={REFRESH_NAME!r}", | ||
| (option(nm) for nm in standard_names), | ||
| optgroup("Carbon", (option(nm) for nm in carbon_names)), | ||
| ) | ||
| ) | ||
| py_code = extract_func_def_embed( | ||
| module_name, | ||
| func_name, | ||
| before=_before_code(REFRESH_NAME, SELECT_ID, TARGET_DIV_ID), | ||
| after=f"{REFRESH_NAME}()", | ||
| assign_to="chart", | ||
| indent=4, | ||
| ) | ||
| results.extend( | ||
| pyscript(packages, TARGET_DIV_ID, loading_label, py_code=py_code) | ||
| ) | ||
| results.append(raw_html("</div></p>\n")) | ||
| return maybe_details( | ||
| results, self.options, default_summary="Show Vega-Altair Theme Test" | ||
| ) | ||
|
|
||
|
|
||
| class PyScriptDirective(SphinxDirective): | ||
| """Placeholder for non-theme related directive.""" | ||
|
|
||
| has_content: ClassVar[Literal[False]] = False | ||
| option_spec = {"packages": directives.unchanged} | ||
|
|
||
| def run(self) -> Sequence[nodes.Node]: | ||
| raise NotImplementedError | ||
|
|
||
|
|
||
| class CodeRefDirective(SphinxDirective): | ||
| """ | ||
| Formatted code block, referencing the contents of a function definition. | ||
|
|
||
| Options: | ||
|
|
||
| .. altair-code-ref:: | ||
| :output: [code, plot] | ||
| :fold: flag | ||
| :summary: str | ||
|
|
||
| Examples | ||
| -------- | ||
| Reference a function, generating a code block: | ||
|
|
||
| .. altair-code-ref:: package.module.function | ||
|
|
||
| Wrap the code block in a collapsible `details`_ tag: | ||
|
|
||
| .. altair-code-ref:: package.module.function | ||
| :fold: | ||
|
|
||
| Override default ``"Show code"`` `details`_ summary: | ||
|
|
||
| .. altair-code-ref:: package.module.function | ||
| :fold: | ||
| :summary: Look here! | ||
|
|
||
| Use `altair-plot`_ instead of a code block: | ||
|
|
||
| .. altair-code-ref:: package.module.function | ||
| :output: plot | ||
|
|
||
| .. note:: | ||
| Using `altair-plot`_ currently ignores the other options. | ||
|
|
||
| .. _details: | ||
| https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details | ||
| .. _altair-plot: | ||
| https://github.com/vega/sphinxext-altair | ||
| """ | ||
|
|
||
| has_content: ClassVar[Literal[False]] = False | ||
| required_arguments: ClassVar[Literal[1]] = 1 | ||
| option_spec: ClassVar[dict[_Option, Callable[[str], Any]]] = { | ||
| "output": validate_output, | ||
| "fold": directives.flag, | ||
| "summary": directives.unchanged_required, | ||
| } | ||
|
|
||
| def __init__( | ||
| self, | ||
| name: str, | ||
| arguments: list[str], | ||
| options: dict[_Option, Any], | ||
| content: StringList, | ||
| lineno: int, | ||
| content_offset: int, | ||
| block_text: str, | ||
| state: RSTState, | ||
| state_machine: RSTStateMachine, | ||
| ) -> None: | ||
| super().__init__(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine) # fmt: skip | ||
| self.options: dict[_Option, Any] | ||
|
|
||
| def run(self) -> Sequence[nodes.Node]: | ||
| qual_name = self.arguments[0] | ||
| module_name, func_name = qual_name.rsplit(".", 1) | ||
| output: _OutputLong = self.options.get("output", "code-block") | ||
| content = extract_func_def(module_name, func_name, output=output) | ||
| parsed = nested_parse_to_nodes(self.state, content) | ||
| return maybe_details(parsed, self.options, default_summary="Show code") | ||
|
|
||
|
|
||
| def setup(app: Sphinx) -> None: | ||
| app.add_directive_to_domain("py", "altair-code-ref", CodeRefDirective) | ||
| app.add_js_file(_PYSCRIPT_URL, loading_method="defer", type="module") | ||
| # app.add_directive("altair-pyscript", PyScriptDirective) | ||
| app.add_directive("altair-theme", ThemeDirective) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.