Skip to content

Commit

Permalink
Merge branch 'main' into shorthand-namespace
Browse files Browse the repository at this point in the history
  • Loading branch information
dangotbanned authored Oct 3, 2024
2 parents b472612 + d4c3bcf commit f981130
Show file tree
Hide file tree
Showing 45 changed files with 16,744 additions and 6,899 deletions.
6 changes: 6 additions & 0 deletions .github/release.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
changelog:
categories:
- title: Breaking
labels:
- breaking
- title: Deprecation
labels:
- deprecation
- title: Enhancements
labels:
- enhancement
Expand Down
2 changes: 1 addition & 1 deletion NOTES_FOR_MAINTAINERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,4 @@ To cut a new release of Altair, follow the steps outlined in

## Web analytics
We use the privacy-friendly [plausible.io](https://plausible.io/) for tracking usage statistics of our documentation.
It is hosted on [https://views.scientific-python.org](https://views.scientific-python.org). To view the stats, you need an account. Ask another maintainer to invite you.
It is hosted on [https://views.scientific-python.org](https://views.scientific-python.org). You can view the stats [here](https://views.scientific-python.org/altair-viz.github.io). To get an account to edit the settings of the web tracking, ask another maintainer.
1 change: 1 addition & 0 deletions altair/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,7 @@
"mixins",
"param",
"parse_shorthand",
"register_theme",
"renderers",
"repeat",
"sample",
Expand Down
2 changes: 1 addition & 1 deletion altair/expr/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,4 +237,4 @@ def __repr__(self) -> str:
return f"{self.group}[{self.name!r}]"


IntoExpression: TypeAlias = Union[bool, None, str, OperatorMixin, Dict[str, Any]]
IntoExpression: TypeAlias = Union[bool, None, str, float, OperatorMixin, Dict[str, Any]]
4 changes: 4 additions & 0 deletions altair/typing.py → altair/typing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,13 @@
"ChartType",
"EncodeKwds",
"Optional",
"ThemeConfig",
"is_chart_type",
"theme",
]

from altair.typing import theme
from altair.typing.theme import ThemeConfig
from altair.utils.schemapi import Optional
from altair.vegalite.v5.api import ChartType, is_chart_type
from altair.vegalite.v5.schema.channels import (
Expand Down
1 change: 1 addition & 0 deletions altair/typing/theme.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from altair.vegalite.v5.schema._config import * # noqa: F403
3 changes: 2 additions & 1 deletion altair/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@
from .deprecation import AltairDeprecationWarning, deprecated, deprecated_warn
from .html import spec_to_html
from .plugin_registry import PluginRegistry
from .schemapi import Optional, SchemaBase, Undefined, is_undefined
from .schemapi import Optional, SchemaBase, SchemaLike, Undefined, is_undefined

__all__ = (
"SHORTHAND_KEYS",
"AltairDeprecationWarning",
"Optional",
"PluginRegistry",
"SchemaBase",
"SchemaLike",
"Undefined",
"deprecated",
"deprecated_warn",
Expand Down
136 changes: 79 additions & 57 deletions altair/utils/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,38 +12,58 @@
from copy import deepcopy
from itertools import groupby
from operator import itemgetter
from typing import TYPE_CHECKING, Any, Callable, Iterator, Literal, TypeVar, cast
from typing import (
TYPE_CHECKING,
Any,
Callable,
Iterator,
Literal,
TypeVar,
cast,
overload,
)

import jsonschema
import narwhals.stable.v1 as nw
from narwhals.dependencies import get_polars, is_pandas_dataframe
from narwhals.typing import IntoDataFrame

from altair.utils.schemapi import SchemaBase, Undefined
from altair.utils.schemapi import SchemaBase, SchemaLike, Undefined

if sys.version_info >= (3, 12):
from typing import Protocol, runtime_checkable
from typing import Protocol, TypeAliasType, runtime_checkable
else:
from typing_extensions import Protocol, runtime_checkable
from typing_extensions import Protocol, TypeAliasType, runtime_checkable
if sys.version_info >= (3, 10):
from typing import ParamSpec
from typing import Concatenate, ParamSpec
else:
from typing_extensions import ParamSpec
from typing_extensions import Concatenate, ParamSpec


if TYPE_CHECKING:
import typing as t
from types import ModuleType

import pandas as pd
from narwhals.typing import IntoExpr

from altair.utils._dfi_types import DataFrame as DfiDataFrame
from altair.vegalite.v5.schema._typing import StandardType_T as InferredVegaLiteType

V = TypeVar("V")
P = ParamSpec("P")
TIntoDataFrame = TypeVar("TIntoDataFrame", bound=IntoDataFrame)
T = TypeVar("T")
P = ParamSpec("P")
R = TypeVar("R")

WrapsFunc = TypeAliasType("WrapsFunc", Callable[..., R], type_params=(R,))
WrappedFunc = TypeAliasType("WrappedFunc", Callable[P, R], type_params=(P, R))
# NOTE: Requires stringized form to avoid `< (3, 11)` issues
# See: https://github.com/vega/altair/actions/runs/10667859416/job/29567290871?pr=3565
WrapsMethod = TypeAliasType(
"WrapsMethod", "Callable[Concatenate[T, ...], R]", type_params=(T, R)
)
WrappedMethod = TypeAliasType(
"WrappedMethod", Callable[Concatenate[T, P], R], type_params=(T, P, R)
)


@runtime_checkable
Expand Down Expand Up @@ -709,39 +729,63 @@ def infer_vegalite_type_for_narwhals(
raise ValueError(msg)


def use_signature(obj: Callable[P, Any]): # -> Callable[..., Callable[P, V]]:
"""Apply call signature and documentation of `obj` to the decorated method."""
def use_signature(tp: Callable[P, Any], /):
"""
Use the signature and doc of ``tp`` for the decorated callable ``cb``.
def decorate(func: Callable[..., V]) -> Callable[P, V]:
# call-signature of func is exposed via __wrapped__.
# we want it to mimic obj.__init__
- **Overload 1**: Decorating method
- **Overload 2**: Decorating function
# error: Accessing "__init__" on an instance is unsound,
# since instance.__init__ could be from an incompatible subclass [misc]
wrapped = (
obj.__init__ if (isinstance(obj, type) and issubclass(obj, object)) else obj # type: ignore [misc]
)
func.__wrapped__ = wrapped # type: ignore[attr-defined]
func._uses_signature = obj # type: ignore[attr-defined]

# Supplement the docstring of func with information from obj
if doc_in := obj.__doc__:
doc_lines = doc_in.splitlines(keepends=True)[1:]
# Patch in a reference to the class this docstring is copied from,
# to generate a hyperlink.
line_1 = f"{func.__doc__ or f'Refer to :class:`{obj.__name__}`'}\n"
func.__doc__ = "".join((line_1, *doc_lines))
return func
Returns
-------
**Adding the annotation breaks typing**:
Overload[Callable[[WrapsMethod[T, R]], WrappedMethod[T, P, R]], Callable[[WrapsFunc[R]], WrappedFunc[P, R]]]
"""

@overload
def decorate(cb: WrapsMethod[T, R], /) -> WrappedMethod[T, P, R]: ...

@overload
def decorate(cb: WrapsFunc[R], /) -> WrappedFunc[P, R]: ...

def decorate(cb: WrapsFunc[R], /) -> WrappedMethod[T, P, R] | WrappedFunc[P, R]:
"""
Raises when no doc was found.
Notes
-----
- Reference to ``tp`` is stored in ``cb.__wrapped__``.
- The doc for ``cb`` will have a ``.rst`` link added, referring to ``tp``.
"""
cb.__wrapped__ = getattr(tp, "__init__", tp) # type: ignore[attr-defined]

if doc_in := tp.__doc__:
line_1 = f"{cb.__doc__ or f'Refer to :class:`{tp.__name__}`'}\n"
cb.__doc__ = "".join((line_1, *doc_in.splitlines(keepends=True)[1:]))
return cb
else:
msg = f"Found no doc for {obj!r}"
msg = f"Found no doc for {tp!r}"
raise AttributeError(msg)

return decorate


@overload
def update_nested(
original: t.MutableMapping[Any, Any],
update: t.Mapping[Any, Any],
copy: Literal[False] = ...,
) -> t.MutableMapping[Any, Any]: ...
@overload
def update_nested(
original: t.Mapping[Any, Any],
update: t.Mapping[Any, Any],
copy: Literal[True],
) -> t.MutableMapping[Any, Any]: ...
def update_nested(
original: Any,
update: t.Mapping[Any, Any],
copy: bool = False,
) -> t.MutableMapping[Any, Any]:
"""
Expand Down Expand Up @@ -812,22 +856,6 @@ class _ChannelCache:
channel_to_name: dict[type[SchemaBase], str]
name_to_channel: dict[str, dict[_ChannelType, type[SchemaBase]]]

@classmethod
def from_channels(cls, channels: ModuleType, /) -> _ChannelCache:
# - This branch is only kept for tests that depend on mocking `channels`.
# - No longer needs to pass around `channels` reference and rebuild every call.
c_to_n = {
c: c._encoding_name
for c in channels.__dict__.values()
if isinstance(c, type)
and issubclass(c, SchemaBase)
and hasattr(c, "_encoding_name")
}
self = cls.__new__(cls)
self.channel_to_name = c_to_n
self.name_to_channel = _invert_group_channels(c_to_n)
return self

@classmethod
def from_cache(cls) -> _ChannelCache:
global _CHANNEL_CACHE
Expand All @@ -853,6 +881,8 @@ def _wrap_in_channel(self, obj: Any, encoding: str, /):
obj = {"shorthand": obj}
elif isinstance(obj, (list, tuple)):
return [self._wrap_in_channel(el, encoding) for el in obj]
elif isinstance(obj, SchemaLike):
obj = obj.to_dict()
if channel := self.name_to_channel.get(encoding):
tp = channel["value" if "value" in obj else "field"]
try:
Expand Down Expand Up @@ -925,9 +955,7 @@ def _reduce(it: Iterator[tuple[type[Any], str]]) -> Any:
return {k: _reduce(chans) for k, chans in grouper}


def infer_encoding_types(
args: tuple[Any, ...], kwargs: dict[str, Any], channels: ModuleType | None = None
):
def infer_encoding_types(args: tuple[Any, ...], kwargs: dict[str, Any]):
"""
Infer typed keyword arguments for args and kwargs.
Expand All @@ -937,20 +965,14 @@ def infer_encoding_types(
Sequence of function args
kwargs : MutableMapping
Dict of function kwargs
channels : ModuleType
The module containing all altair encoding channel classes.
Returns
-------
kwargs : dict
All args and kwargs in a single dict, with keys and types
based on the channels mapping.
"""
cache = (
_ChannelCache.from_channels(channels)
if channels
else _ChannelCache.from_cache()
)
cache = _ChannelCache.from_cache()
# First use the mapping to convert args to kwargs based on their types.
for arg in args:
el = next(iter(arg), None) if isinstance(arg, (list, tuple)) else arg
Expand Down
47 changes: 44 additions & 3 deletions altair/utils/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from altair.utils._importers import import_vl_convert, vl_version_for_vl_convert

TemplateName = Literal["standard", "universal", "inline"]
TemplateName = Literal["standard", "universal", "inline", "olli"]
RenderMode = Literal["vega", "vega-lite"]

HTML_TEMPLATE = jinja2.Template(
Expand All @@ -16,6 +16,7 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
{%- endif %}
<style>
#{{ output_div }}.vega-embed {
Expand Down Expand Up @@ -116,11 +117,22 @@
if (outputDiv.id !== "{{ output_div }}") {
outputDiv = document.getElementById("{{ output_div }}");
}
{%- if use_olli %}
const olliDiv = document.createElement("div");
const vegaDiv = document.createElement("div");
outputDiv.appendChild(vegaDiv);
outputDiv.appendChild(olliDiv);
outputDiv = vegaDiv;
{%- endif %}
const paths = {
"vega": "{{ base_url }}/vega@{{ vega_version }}?noext",
"vega-lib": "{{ base_url }}/vega-lib?noext",
"vega-lite": "{{ base_url }}/vega-lite@{{ vegalite_version }}?noext",
"vega-embed": "{{ base_url }}/vega-embed@{{ vegaembed_version }}?noext",
{%- if use_olli %}
"olli": "{{ base_url }}/olli@{{ olli_version }}?noext",
"olli-adapters": "{{ base_url }}/olli-adapters@{{ olli_adapters_version }}?noext",
{%- endif %}
};
function maybeLoadScript(lib, version) {
Expand All @@ -145,20 +157,41 @@
throw err;
}
function displayChart(vegaEmbed) {
function displayChart(vegaEmbed, olli, olliAdapters) {
vegaEmbed(outputDiv, spec, embedOpt)
.catch(err => showError(`Javascript Error: ${err.message}<br>This usually means there's a typo in your chart specification. See the javascript console for the full traceback.`));
{%- if use_olli %}
olliAdapters.VegaLiteAdapter(spec).then(olliVisSpec => {
// It's a function if it was loaded via maybeLoadScript below.
// If it comes from require, it's a module and we access olli.olli
const olliFunc = typeof olli === 'function' ? olli : olli.olli;
const olliRender = olliFunc(olliVisSpec);
olliDiv.append(olliRender);
});
{%- endif %}
}
if(typeof define === "function" && define.amd) {
requirejs.config({paths});
require(["vega-embed"], displayChart, err => showError(`Error loading script: ${err.message}`));
let deps = ["vega-embed"];
{%- if use_olli %}
deps.push("olli", "olli-adapters");
{%- endif %}
require(deps, displayChart, err => showError(`Error loading script: ${err.message}`));
} else {
maybeLoadScript("vega", "{{vega_version}}")
.then(() => maybeLoadScript("vega-lite", "{{vegalite_version}}"))
.then(() => maybeLoadScript("vega-embed", "{{vegaembed_version}}"))
{%- if use_olli %}
.then(() => maybeLoadScript("olli", "{{olli_version}}"))
.then(() => maybeLoadScript("olli-adapters", "{{olli_adapters_version}}"))
{%- endif %}
.catch(showError)
{%- if use_olli %}
.then(() => displayChart(vegaEmbed, olli, OlliAdapters));
{%- else %}
.then(() => displayChart(vegaEmbed));
{%- endif %}
}
})({{ spec }}, {{ embed_options }});
</script>
Expand All @@ -176,6 +209,7 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
#{{ output_div }}.vega-embed {
width: 100%;
Expand Down Expand Up @@ -209,6 +243,7 @@
"standard": HTML_TEMPLATE,
"universal": HTML_TEMPLATE_UNIVERSAL,
"inline": INLINE_HTML_TEMPLATE,
"olli": HTML_TEMPLATE_UNIVERSAL,
}


Expand Down Expand Up @@ -293,6 +328,12 @@ def spec_to_html(
vlc = import_vl_convert()
vl_version = vl_version_for_vl_convert()
render_kwargs["vegaembed_script"] = vlc.javascript_bundle(vl_version=vl_version)
elif template == "olli":
OLLI_VERSION = "2"
OLLI_ADAPTERS_VERSION = "2"
render_kwargs["olli_version"] = OLLI_VERSION
render_kwargs["olli_adapters_version"] = OLLI_ADAPTERS_VERSION
render_kwargs["use_olli"] = True

jinja_template = TEMPLATES.get(template, template) # type: ignore[arg-type]
if not hasattr(jinja_template, "render"):
Expand Down
Loading

0 comments on commit f981130

Please sign in to comment.