Skip to content

Commit

Permalink
refactor: alt.themes -> alt.theme (#3618)
Browse files Browse the repository at this point in the history
  • Loading branch information
dangotbanned authored Nov 11, 2024
1 parent 7b3d479 commit 255f736
Show file tree
Hide file tree
Showing 21 changed files with 2,268 additions and 311 deletions.
38 changes: 35 additions & 3 deletions altair/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -617,7 +617,6 @@
"mixins",
"param",
"parse_shorthand",
"register_theme",
"renderers",
"repeat",
"sample",
Expand All @@ -627,7 +626,6 @@
"sequence",
"sphere",
"theme",
"themes",
"to_csv",
"to_json",
"to_values",
Expand All @@ -653,10 +651,44 @@ def __dir__():
from altair.jupyter import JupyterChart
from altair.expr import expr
from altair.utils import AltairDeprecationWarning, parse_shorthand, Undefined
from altair import typing
from altair import typing, theme


def load_ipython_extension(ipython):
from altair._magics import vegalite

ipython.register_magic_function(vegalite, "cell")


def __getattr__(name: str):
from altair.utils.deprecation import deprecated_warn

if name == "themes":
deprecated_warn(
"Most cases require only the following change:\n\n"
" # Deprecated\n"
" alt.themes.enable('quartz')\n\n"
" # Updated\n"
" alt.theme.enable('quartz')\n\n"
"If your code registers a theme, make the following change:\n\n"
" # Deprecated\n"
" def custom_theme():\n"
" return {'height': 400, 'width': 700}\n"
" alt.themes.register('theme_name', custom_theme)\n"
" alt.themes.enable('theme_name')\n\n"
" # Updated\n"
" @alt.theme.register('theme_name', enable=True)\n"
" def custom_theme() -> alt.theme.ThemeConfig:\n"
" return {'height': 400, 'width': 700}\n\n"
"See the updated User Guide for further details:\n"
" https://altair-viz.github.io/user_guide/api.html#theme\n"
" https://altair-viz.github.io/user_guide/customization.html#chart-themes",
version="5.5.0",
alternative="altair.theme",
stacklevel=3,
action="once",
)
return theme._themes
else:
msg = f"module {__name__!r} has no attribute {name!r}"
raise AttributeError(msg)
321 changes: 321 additions & 0 deletions altair/theme.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,321 @@
"""Customizing chart configuration defaults."""

from __future__ import annotations

from functools import wraps as _wraps
from typing import TYPE_CHECKING, Any
from typing import overload as _overload

from altair.vegalite.v5.schema._config import (
AreaConfigKwds,
AutoSizeParamsKwds,
AxisConfigKwds,
AxisResolveMapKwds,
BarConfigKwds,
BindCheckboxKwds,
BindDirectKwds,
BindInputKwds,
BindRadioSelectKwds,
BindRangeKwds,
BoxPlotConfigKwds,
BrushConfigKwds,
CompositionConfigKwds,
ConfigKwds,
DateTimeKwds,
DerivedStreamKwds,
ErrorBandConfigKwds,
ErrorBarConfigKwds,
FeatureGeometryGeoJsonPropertiesKwds,
FormatConfigKwds,
GeoJsonFeatureCollectionKwds,
GeoJsonFeatureKwds,
GeometryCollectionKwds,
GradientStopKwds,
HeaderConfigKwds,
IntervalSelectionConfigKwds,
IntervalSelectionConfigWithoutTypeKwds,
LegendConfigKwds,
LegendResolveMapKwds,
LegendStreamBindingKwds,
LinearGradientKwds,
LineConfigKwds,
LineStringKwds,
LocaleKwds,
MarkConfigKwds,
MergedStreamKwds,
MultiLineStringKwds,
MultiPointKwds,
MultiPolygonKwds,
NumberLocaleKwds,
OverlayMarkDefKwds,
PaddingKwds,
PointKwds,
PointSelectionConfigKwds,
PointSelectionConfigWithoutTypeKwds,
PolygonKwds,
ProjectionConfigKwds,
ProjectionKwds,
RadialGradientKwds,
RangeConfigKwds,
RectConfigKwds,
ResolveKwds,
RowColKwds,
ScaleConfigKwds,
ScaleInvalidDataConfigKwds,
ScaleResolveMapKwds,
SelectionConfigKwds,
StepKwds,
StyleConfigIndexKwds,
ThemeConfig,
TickConfigKwds,
TimeIntervalStepKwds,
TimeLocaleKwds,
TitleConfigKwds,
TitleParamsKwds,
TooltipContentKwds,
TopLevelSelectionParameterKwds,
VariableParameterKwds,
ViewBackgroundKwds,
ViewConfigKwds,
)
from altair.vegalite.v5.theme import themes as _themes

if TYPE_CHECKING:
import sys
from typing import Any, Callable, Literal

if sys.version_info >= (3, 11):
from typing import LiteralString
else:
from typing_extensions import LiteralString
if sys.version_info >= (3, 10):
from typing import ParamSpec
else:
from typing_extensions import ParamSpec

from altair.utils.plugin_registry import Plugin

P = ParamSpec("P")

__all__ = [
"AreaConfigKwds",
"AutoSizeParamsKwds",
"AxisConfigKwds",
"AxisResolveMapKwds",
"BarConfigKwds",
"BindCheckboxKwds",
"BindDirectKwds",
"BindInputKwds",
"BindRadioSelectKwds",
"BindRangeKwds",
"BoxPlotConfigKwds",
"BrushConfigKwds",
"CompositionConfigKwds",
"ConfigKwds",
"DateTimeKwds",
"DerivedStreamKwds",
"ErrorBandConfigKwds",
"ErrorBarConfigKwds",
"FeatureGeometryGeoJsonPropertiesKwds",
"FormatConfigKwds",
"GeoJsonFeatureCollectionKwds",
"GeoJsonFeatureKwds",
"GeometryCollectionKwds",
"GradientStopKwds",
"HeaderConfigKwds",
"IntervalSelectionConfigKwds",
"IntervalSelectionConfigWithoutTypeKwds",
"LegendConfigKwds",
"LegendResolveMapKwds",
"LegendStreamBindingKwds",
"LineConfigKwds",
"LineStringKwds",
"LinearGradientKwds",
"LocaleKwds",
"MarkConfigKwds",
"MergedStreamKwds",
"MultiLineStringKwds",
"MultiPointKwds",
"MultiPolygonKwds",
"NumberLocaleKwds",
"OverlayMarkDefKwds",
"PaddingKwds",
"PointKwds",
"PointSelectionConfigKwds",
"PointSelectionConfigWithoutTypeKwds",
"PolygonKwds",
"ProjectionConfigKwds",
"ProjectionKwds",
"RadialGradientKwds",
"RangeConfigKwds",
"RectConfigKwds",
"ResolveKwds",
"RowColKwds",
"ScaleConfigKwds",
"ScaleInvalidDataConfigKwds",
"ScaleResolveMapKwds",
"SelectionConfigKwds",
"StepKwds",
"StyleConfigIndexKwds",
"ThemeConfig",
"TickConfigKwds",
"TimeIntervalStepKwds",
"TimeLocaleKwds",
"TitleConfigKwds",
"TitleParamsKwds",
"TooltipContentKwds",
"TopLevelSelectionParameterKwds",
"VariableParameterKwds",
"ViewBackgroundKwds",
"ViewConfigKwds",
"active",
"enable",
"get",
"names",
"options",
"register",
"unregister",
]


def register(
name: LiteralString, *, enable: bool
) -> Callable[[Plugin[ThemeConfig]], Plugin[ThemeConfig]]:
"""
Decorator for registering a theme function.
Parameters
----------
name
Unique name assigned in registry.
enable
Auto-enable the wrapped theme.
Examples
--------
Register and enable a theme::
import altair as alt
from altair import theme
@theme.register("param_font_size", enable=True)
def custom_theme() -> theme.ThemeConfig:
sizes = 12, 14, 16, 18, 20
return {
"autosize": {"contains": "content", "resize": True},
"background": "#F3F2F1",
"config": {
"axisX": {"labelFontSize": sizes[1], "titleFontSize": sizes[1]},
"axisY": {"labelFontSize": sizes[1], "titleFontSize": sizes[1]},
"font": "'Lato', 'Segoe UI', Tahoma, Verdana, sans-serif",
"headerColumn": {"labelFontSize": sizes[1]},
"headerFacet": {"labelFontSize": sizes[1]},
"headerRow": {"labelFontSize": sizes[1]},
"legend": {"labelFontSize": sizes[0], "titleFontSize": sizes[1]},
"text": {"fontSize": sizes[0]},
"title": {"fontSize": sizes[-1]},
},
"height": {"step": 28},
"width": 350,
}
We can then see the ``name`` parameter displayed when checking::
theme.active
"param_font_size"
Until another theme has been enabled, all charts will use defaults set in ``custom_theme()``::
from vega_datasets import data
source = data.stocks()
lines = (
alt.Chart(source, title=alt.Title("Stocks"))
.mark_line()
.encode(x="date:T", y="price:Q", color="symbol:N")
)
lines.interactive(bind_y=False)
"""

# HACK: See for `LiteralString` requirement in `name`
# https://github.com/vega/altair/pull/3526#discussion_r1743350127
def decorate(func: Plugin[ThemeConfig], /) -> Plugin[ThemeConfig]:
_register(name, func)
if enable:
_themes.enable(name)

@_wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> ThemeConfig:
return func(*args, **kwargs)

return wrapper

return decorate


def unregister(name: LiteralString) -> Plugin[ThemeConfig]:
"""
Remove and return a previously registered theme.
Parameters
----------
name
Unique name assigned during ``alt.theme.register``.
Raises
------
TypeError
When ``name`` has not been registered.
"""
plugin = _register(name, None)
if plugin is None:
msg = (
f"Found no theme named {name!r} in registry.\n"
f"Registered themes:\n"
f"{names()!r}"
)
raise TypeError(msg)
else:
return plugin


enable = _themes.enable
get = _themes.get
names = _themes.names
active: str
"""Return the name of the currently active theme."""
options: dict[str, Any]
"""Return the current themes options dictionary."""


def __dir__() -> list[str]:
return __all__


@_overload
def __getattr__(name: Literal["active"]) -> str: ... # type: ignore[misc]
@_overload
def __getattr__(name: Literal["options"]) -> dict[str, Any]: ... # type: ignore[misc]
def __getattr__(name: str) -> Any:
if name == "active":
return _themes.active
elif name == "options":
return _themes.options
else:
msg = f"module {__name__!r} has no attribute {name!r}"
raise AttributeError(msg)


def _register(
name: LiteralString, fn: Plugin[ThemeConfig] | None, /
) -> Plugin[ThemeConfig] | None:
if fn is None:
return _themes._plugins.pop(name, None)
elif _themes.plugin_type(fn):
_themes._plugins[name] = fn
return fn
else:
msg = f"{type(fn).__name__!r} is not a callable theme\n\n{fn!r}"
raise TypeError(msg)
4 changes: 0 additions & 4 deletions altair/typing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,9 @@
"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: 0 additions & 1 deletion altair/typing/theme.py

This file was deleted.

Loading

0 comments on commit 255f736

Please sign in to comment.