Mise à jour de Monitor.py et autres scripts

This commit is contained in:
Debian
2025-07-23 10:46:27 +02:00
parent 7081418ce0
commit 7de3e0fb50
8604 changed files with 2789953 additions and 295 deletions

View File

@@ -0,0 +1,696 @@
# ruff: noqa
__version__ = "5.5.0"
# The content of __all__ is automatically written by
# tools/update_init_file.py. Do not modify directly.
__all__ = [
"Aggregate",
"AggregateOp",
"AggregateTransform",
"AggregatedFieldDef",
"Align",
"AllSortString",
"AltairDeprecationWarning",
"Angle",
"AngleDatum",
"AngleValue",
"AnyMark",
"AnyMarkConfig",
"AreaConfig",
"ArgmaxDef",
"ArgminDef",
"AutoSizeParams",
"AutosizeType",
"Axis",
"AxisConfig",
"AxisOrient",
"AxisResolveMap",
"BBox",
"BarConfig",
"BaseTitleNoValueRefs",
"Baseline",
"Bin",
"BinExtent",
"BinParams",
"BinTransform",
"BindCheckbox",
"BindDirect",
"BindInput",
"BindRadioSelect",
"BindRange",
"Binding",
"BinnedTimeUnit",
"Blend",
"BoxPlot",
"BoxPlotConfig",
"BoxPlotDef",
"BrushConfig",
"CalculateTransform",
"Categorical",
"ChainedWhen",
"Chart",
"ChartDataType",
"Color",
"ColorDatum",
"ColorDef",
"ColorName",
"ColorScheme",
"ColorValue",
"Column",
"CompositeMark",
"CompositeMarkDef",
"CompositionConfig",
"ConcatChart",
"ConcatSpecGenericSpec",
"ConditionalAxisColor",
"ConditionalAxisLabelAlign",
"ConditionalAxisLabelBaseline",
"ConditionalAxisLabelFontStyle",
"ConditionalAxisLabelFontWeight",
"ConditionalAxisNumber",
"ConditionalAxisNumberArray",
"ConditionalAxisPropertyAlignnull",
"ConditionalAxisPropertyColornull",
"ConditionalAxisPropertyFontStylenull",
"ConditionalAxisPropertyFontWeightnull",
"ConditionalAxisPropertyTextBaselinenull",
"ConditionalAxisPropertynumberArraynull",
"ConditionalAxisPropertynumbernull",
"ConditionalAxisPropertystringnull",
"ConditionalAxisString",
"ConditionalMarkPropFieldOrDatumDef",
"ConditionalMarkPropFieldOrDatumDefTypeForShape",
"ConditionalParameterMarkPropFieldOrDatumDef",
"ConditionalParameterMarkPropFieldOrDatumDefTypeForShape",
"ConditionalParameterStringFieldDef",
"ConditionalParameterValueDefGradientstringnullExprRef",
"ConditionalParameterValueDefTextExprRef",
"ConditionalParameterValueDefnumber",
"ConditionalParameterValueDefnumberArrayExprRef",
"ConditionalParameterValueDefnumberExprRef",
"ConditionalParameterValueDefstringExprRef",
"ConditionalParameterValueDefstringnullExprRef",
"ConditionalPredicateMarkPropFieldOrDatumDef",
"ConditionalPredicateMarkPropFieldOrDatumDefTypeForShape",
"ConditionalPredicateStringFieldDef",
"ConditionalPredicateValueDefAlignnullExprRef",
"ConditionalPredicateValueDefColornullExprRef",
"ConditionalPredicateValueDefFontStylenullExprRef",
"ConditionalPredicateValueDefFontWeightnullExprRef",
"ConditionalPredicateValueDefGradientstringnullExprRef",
"ConditionalPredicateValueDefTextBaselinenullExprRef",
"ConditionalPredicateValueDefTextExprRef",
"ConditionalPredicateValueDefnumber",
"ConditionalPredicateValueDefnumberArrayExprRef",
"ConditionalPredicateValueDefnumberArraynullExprRef",
"ConditionalPredicateValueDefnumberExprRef",
"ConditionalPredicateValueDefnumbernullExprRef",
"ConditionalPredicateValueDefstringExprRef",
"ConditionalPredicateValueDefstringnullExprRef",
"ConditionalStringFieldDef",
"ConditionalValueDefGradientstringnullExprRef",
"ConditionalValueDefTextExprRef",
"ConditionalValueDefnumber",
"ConditionalValueDefnumberArrayExprRef",
"ConditionalValueDefnumberExprRef",
"ConditionalValueDefstringExprRef",
"ConditionalValueDefstringnullExprRef",
"Config",
"CsvDataFormat",
"Cursor",
"Cyclical",
"Data",
"DataFormat",
"DataSource",
"DataType",
"Datasets",
"DateTime",
"DatumChannelMixin",
"DatumDef",
"Day",
"DensityTransform",
"DerivedStream",
"Description",
"DescriptionValue",
"Detail",
"Dict",
"DictInlineDataset",
"DictSelectionInit",
"DictSelectionInitInterval",
"Diverging",
"DomainUnionWith",
"DsvDataFormat",
"Element",
"Encoding",
"EncodingSortField",
"ErrorBand",
"ErrorBandConfig",
"ErrorBandDef",
"ErrorBar",
"ErrorBarConfig",
"ErrorBarDef",
"ErrorBarExtent",
"EventStream",
"EventType",
"Expr",
"ExprRef",
"ExtentTransform",
"Facet",
"FacetChart",
"FacetEncodingFieldDef",
"FacetFieldDef",
"FacetMapping",
"FacetSpec",
"FacetedEncoding",
"FacetedUnitSpec",
"Feature",
"FeatureCollection",
"FeatureGeometryGeoJsonProperties",
"Field",
"FieldChannelMixin",
"FieldDefWithoutScale",
"FieldEqualPredicate",
"FieldGTEPredicate",
"FieldGTPredicate",
"FieldLTEPredicate",
"FieldLTPredicate",
"FieldName",
"FieldOneOfPredicate",
"FieldOrDatumDefWithConditionDatumDefGradientstringnull",
"FieldOrDatumDefWithConditionDatumDefnumber",
"FieldOrDatumDefWithConditionDatumDefnumberArray",
"FieldOrDatumDefWithConditionDatumDefstringnull",
"FieldOrDatumDefWithConditionMarkPropFieldDefGradientstringnull",
"FieldOrDatumDefWithConditionMarkPropFieldDefTypeForShapestringnull",
"FieldOrDatumDefWithConditionMarkPropFieldDefnumber",
"FieldOrDatumDefWithConditionMarkPropFieldDefnumberArray",
"FieldOrDatumDefWithConditionStringDatumDefText",
"FieldOrDatumDefWithConditionStringFieldDefText",
"FieldOrDatumDefWithConditionStringFieldDefstring",
"FieldRange",
"FieldRangePredicate",
"FieldValidPredicate",
"Fill",
"FillDatum",
"FillOpacity",
"FillOpacityDatum",
"FillOpacityValue",
"FillValue",
"FilterTransform",
"Fit",
"FlattenTransform",
"FoldTransform",
"FontStyle",
"FontWeight",
"FormatConfig",
"Generator",
"GenericUnitSpecEncodingAnyMark",
"GeoJsonFeature",
"GeoJsonFeatureCollection",
"GeoJsonProperties",
"Geometry",
"GeometryCollection",
"Gradient",
"GradientStop",
"GraticuleGenerator",
"GraticuleParams",
"HConcatChart",
"HConcatSpecGenericSpec",
"Header",
"HeaderConfig",
"HexColor",
"Href",
"HrefValue",
"Impute",
"ImputeMethod",
"ImputeParams",
"ImputeSequence",
"ImputeTransform",
"InlineData",
"InlineDataset",
"Interpolate",
"IntervalSelectionConfig",
"IntervalSelectionConfigWithoutType",
"JoinAggregateFieldDef",
"JoinAggregateTransform",
"JsonDataFormat",
"JupyterChart",
"Key",
"LabelOverlap",
"LatLongDef",
"LatLongFieldDef",
"Latitude",
"Latitude2",
"Latitude2Datum",
"Latitude2Value",
"LatitudeDatum",
"LayerChart",
"LayerRepeatMapping",
"LayerRepeatSpec",
"LayerSpec",
"LayoutAlign",
"Legend",
"LegendBinding",
"LegendConfig",
"LegendOrient",
"LegendResolveMap",
"LegendStreamBinding",
"LineConfig",
"LineString",
"LinearGradient",
"LocalMultiTimeUnit",
"LocalSingleTimeUnit",
"Locale",
"LoessTransform",
"LogicalAndPredicate",
"LogicalNotPredicate",
"LogicalOrPredicate",
"Longitude",
"Longitude2",
"Longitude2Datum",
"Longitude2Value",
"LongitudeDatum",
"LookupData",
"LookupSelection",
"LookupTransform",
"Mark",
"MarkConfig",
"MarkDef",
"MarkInvalidDataMode",
"MarkPropDefGradientstringnull",
"MarkPropDefnumber",
"MarkPropDefnumberArray",
"MarkPropDefstringnullTypeForShape",
"MarkType",
"MaxRowsError",
"MergedStream",
"Month",
"MultiLineString",
"MultiPoint",
"MultiPolygon",
"MultiTimeUnit",
"NamedData",
"NonArgAggregateOp",
"NonLayerRepeatSpec",
"NonNormalizedSpec",
"NumberLocale",
"NumericArrayMarkPropDef",
"NumericMarkPropDef",
"OffsetDef",
"Opacity",
"OpacityDatum",
"OpacityValue",
"Order",
"OrderFieldDef",
"OrderOnlyDef",
"OrderValue",
"OrderValueDef",
"Orient",
"Orientation",
"OverlayMarkDef",
"Padding",
"Parameter",
"ParameterExpression",
"ParameterExtent",
"ParameterName",
"ParameterPredicate",
"Parse",
"ParseValue",
"PivotTransform",
"Point",
"PointSelectionConfig",
"PointSelectionConfigWithoutType",
"PolarDef",
"Polygon",
"Position",
"Position2Def",
"PositionDatumDef",
"PositionDatumDefBase",
"PositionDef",
"PositionFieldDef",
"PositionFieldDefBase",
"PositionValueDef",
"Predicate",
"PredicateComposition",
"PrimitiveValue",
"Projection",
"ProjectionConfig",
"ProjectionType",
"QuantileTransform",
"RadialGradient",
"Radius",
"Radius2",
"Radius2Datum",
"Radius2Value",
"RadiusDatum",
"RadiusValue",
"RangeConfig",
"RangeEnum",
"RangeRaw",
"RangeRawArray",
"RangeScheme",
"RectConfig",
"RegressionTransform",
"RelativeBandSize",
"RepeatChart",
"RepeatMapping",
"RepeatRef",
"RepeatSpec",
"Resolve",
"ResolveMode",
"Root",
"Row",
"RowColLayoutAlign",
"RowColboolean",
"RowColnumber",
"RowColumnEncodingFieldDef",
"SCHEMA_URL",
"SCHEMA_VERSION",
"SampleTransform",
"Scale",
"ScaleBinParams",
"ScaleBins",
"ScaleConfig",
"ScaleDatumDef",
"ScaleFieldDef",
"ScaleInterpolateEnum",
"ScaleInterpolateParams",
"ScaleInvalidDataConfig",
"ScaleInvalidDataShowAsValueangle",
"ScaleInvalidDataShowAsValuecolor",
"ScaleInvalidDataShowAsValuefill",
"ScaleInvalidDataShowAsValuefillOpacity",
"ScaleInvalidDataShowAsValueopacity",
"ScaleInvalidDataShowAsValueradius",
"ScaleInvalidDataShowAsValueshape",
"ScaleInvalidDataShowAsValuesize",
"ScaleInvalidDataShowAsValuestroke",
"ScaleInvalidDataShowAsValuestrokeDash",
"ScaleInvalidDataShowAsValuestrokeOpacity",
"ScaleInvalidDataShowAsValuestrokeWidth",
"ScaleInvalidDataShowAsValuetheta",
"ScaleInvalidDataShowAsValuex",
"ScaleInvalidDataShowAsValuexOffset",
"ScaleInvalidDataShowAsValuey",
"ScaleInvalidDataShowAsValueyOffset",
"ScaleInvalidDataShowAsangle",
"ScaleInvalidDataShowAscolor",
"ScaleInvalidDataShowAsfill",
"ScaleInvalidDataShowAsfillOpacity",
"ScaleInvalidDataShowAsopacity",
"ScaleInvalidDataShowAsradius",
"ScaleInvalidDataShowAsshape",
"ScaleInvalidDataShowAssize",
"ScaleInvalidDataShowAsstroke",
"ScaleInvalidDataShowAsstrokeDash",
"ScaleInvalidDataShowAsstrokeOpacity",
"ScaleInvalidDataShowAsstrokeWidth",
"ScaleInvalidDataShowAstheta",
"ScaleInvalidDataShowAsx",
"ScaleInvalidDataShowAsxOffset",
"ScaleInvalidDataShowAsy",
"ScaleInvalidDataShowAsyOffset",
"ScaleResolveMap",
"ScaleType",
"SchemaBase",
"SchemeParams",
"SecondaryFieldDef",
"SelectionConfig",
"SelectionExpression",
"SelectionInit",
"SelectionInitInterval",
"SelectionInitIntervalMapping",
"SelectionInitMapping",
"SelectionParameter",
"SelectionPredicateComposition",
"SelectionResolution",
"SelectionType",
"SequenceGenerator",
"SequenceParams",
"SequentialMultiHue",
"SequentialSingleHue",
"Shape",
"ShapeDatum",
"ShapeDef",
"ShapeValue",
"SharedEncoding",
"SingleDefUnitChannel",
"SingleTimeUnit",
"Size",
"SizeDatum",
"SizeValue",
"Sort",
"SortArray",
"SortByChannel",
"SortByChannelDesc",
"SortByEncoding",
"SortField",
"SortOrder",
"Spec",
"SphereGenerator",
"StackOffset",
"StackTransform",
"StandardType",
"Step",
"StepFor",
"Stream",
"StringFieldDef",
"StringFieldDefWithCondition",
"StringValueDefWithCondition",
"Stroke",
"StrokeCap",
"StrokeDash",
"StrokeDashDatum",
"StrokeDashValue",
"StrokeDatum",
"StrokeJoin",
"StrokeOpacity",
"StrokeOpacityDatum",
"StrokeOpacityValue",
"StrokeValue",
"StrokeWidth",
"StrokeWidthDatum",
"StrokeWidthValue",
"StyleConfigIndex",
"SymbolShape",
"TOPLEVEL_ONLY_KEYS",
"Text",
"TextBaseline",
"TextDatum",
"TextDef",
"TextDirection",
"TextValue",
"Then",
"Theta",
"Theta2",
"Theta2Datum",
"Theta2Value",
"ThetaDatum",
"ThetaValue",
"TickConfig",
"TickCount",
"TimeInterval",
"TimeIntervalStep",
"TimeLocale",
"TimeUnit",
"TimeUnitParams",
"TimeUnitTransform",
"TimeUnitTransformParams",
"Title",
"TitleAnchor",
"TitleConfig",
"TitleFrame",
"TitleOrient",
"TitleParams",
"Tooltip",
"TooltipContent",
"TooltipValue",
"TopLevelConcatSpec",
"TopLevelFacetSpec",
"TopLevelHConcatSpec",
"TopLevelLayerSpec",
"TopLevelMixin",
"TopLevelParameter",
"TopLevelRepeatSpec",
"TopLevelSelectionParameter",
"TopLevelSpec",
"TopLevelUnitSpec",
"TopLevelVConcatSpec",
"TopoDataFormat",
"Transform",
"Type",
"TypeForShape",
"TypedFieldDef",
"URI",
"Undefined",
"UnitSpec",
"UnitSpecWithFrame",
"Url",
"UrlData",
"UrlValue",
"UtcMultiTimeUnit",
"UtcSingleTimeUnit",
"VConcatChart",
"VConcatSpecGenericSpec",
"VEGAEMBED_VERSION",
"VEGALITE_VERSION",
"VEGA_VERSION",
"ValueChannelMixin",
"ValueDefWithConditionMarkPropFieldOrDatumDefGradientstringnull",
"ValueDefWithConditionMarkPropFieldOrDatumDefTypeForShapestringnull",
"ValueDefWithConditionMarkPropFieldOrDatumDefnumber",
"ValueDefWithConditionMarkPropFieldOrDatumDefnumberArray",
"ValueDefWithConditionMarkPropFieldOrDatumDefstringnull",
"ValueDefWithConditionStringFieldDefText",
"ValueDefnumber",
"ValueDefnumberwidthheightExprRef",
"VariableParameter",
"Vector10string",
"Vector12string",
"Vector2DateTime",
"Vector2Vector2number",
"Vector2boolean",
"Vector2number",
"Vector2string",
"Vector3number",
"Vector7string",
"VegaLite",
"VegaLiteSchema",
"ViewBackground",
"ViewConfig",
"When",
"WindowEventType",
"WindowFieldDef",
"WindowOnlyOp",
"WindowTransform",
"X",
"X2",
"X2Datum",
"X2Value",
"XDatum",
"XError",
"XError2",
"XError2Value",
"XErrorValue",
"XOffset",
"XOffsetDatum",
"XOffsetValue",
"XValue",
"Y",
"Y2",
"Y2Datum",
"Y2Value",
"YDatum",
"YError",
"YError2",
"YError2Value",
"YErrorValue",
"YOffset",
"YOffsetDatum",
"YOffsetValue",
"YValue",
"api",
"binding",
"binding_checkbox",
"binding_radio",
"binding_range",
"binding_select",
"channels",
"check_fields_and_encodings",
"compiler",
"concat",
"condition",
"core",
"data",
"data_transformers",
"datum",
"default_data_transformer",
"display",
"expr",
"graticule",
"hconcat",
"jupyter",
"layer",
"limit_rows",
"load_ipython_extension",
"load_schema",
"mixins",
"param",
"parse_shorthand",
"renderers",
"repeat",
"sample",
"schema",
"selection_interval",
"selection_point",
"sequence",
"sphere",
"theme",
"to_csv",
"to_json",
"to_values",
"topo_feature",
"typing",
"utils",
"v5",
"value",
"vconcat",
"vegalite",
"vegalite_compilers",
"when",
"with_property_setters",
]
def __dir__():
return __all__
from altair.vegalite import *
from altair.vegalite.v5.schema.core import Dict
from altair.jupyter import JupyterChart
from altair.expr import expr
from altair.utils import AltairDeprecationWarning, parse_shorthand, Undefined
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():\n"
" return alt.theme.ThemeConfig(\n"
" {'height': 400, 'width': 700}\n"
" )\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)

View File

@@ -0,0 +1,109 @@
"""Magic functions for rendering vega-lite specifications."""
from __future__ import annotations
import json
import warnings
from importlib.util import find_spec
from typing import Any
from IPython.core import magic_arguments
from narwhals.stable.v1.dependencies import is_pandas_dataframe
from altair.vegalite import v5 as vegalite_v5
__all__ = ["vegalite"]
RENDERERS = {
"vega-lite": {
"5": vegalite_v5.VegaLite,
},
}
TRANSFORMERS = {
"vega-lite": {
"5": vegalite_v5.data_transformers,
},
}
def _prepare_data(data, data_transformers):
"""Convert input data to data for use within schema."""
if data is None or isinstance(data, dict):
return data
elif is_pandas_dataframe(data):
if func := data_transformers.get():
data = func(data)
return data
elif isinstance(data, str):
return {"url": data}
else:
warnings.warn(f"data of type {type(data)} not recognized", stacklevel=1)
return data
def _get_variable(name: str) -> Any:
"""Get a variable from the notebook namespace."""
from IPython.core.getipython import get_ipython
if ip := get_ipython():
if name not in ip.user_ns:
msg = f"argument '{name}' does not match the name of any defined variable"
raise NameError(msg)
return ip.user_ns[name]
else:
msg = (
"Magic command must be run within an IPython "
"environment, in which get_ipython() is defined."
)
raise ValueError(msg)
@magic_arguments.magic_arguments()
@magic_arguments.argument(
"data",
nargs="?",
help="local variablename of a pandas DataFrame to be used as the dataset",
)
@magic_arguments.argument("-v", "--version", dest="version", default="v5")
@magic_arguments.argument("-j", "--json", dest="json", action="store_true")
def vegalite(line, cell) -> vegalite_v5.VegaLite:
"""
Cell magic for displaying vega-lite visualizations in CoLab.
%%vegalite [dataframe] [--json] [--version='v5']
Visualize the contents of the cell using Vega-Lite, optionally
specifying a pandas DataFrame object to be used as the dataset.
if --json is passed, then input is parsed as json rather than yaml.
"""
args = magic_arguments.parse_argstring(vegalite, line)
existing_versions = {"v5": "5"}
version = existing_versions[args.version]
assert version in RENDERERS["vega-lite"]
VegaLite = RENDERERS["vega-lite"][version]
data_transformers = TRANSFORMERS["vega-lite"][version]
if args.json:
spec = json.loads(cell)
elif not find_spec("yaml"):
try:
spec = json.loads(cell)
except json.JSONDecodeError as err:
msg = (
"%%vegalite: spec is not valid JSON. "
"Install pyyaml to parse spec as yaml"
)
raise ValueError(msg) from err
else:
import yaml
spec = yaml.load(cell, Loader=yaml.SafeLoader)
if args.data is not None:
data = _get_variable(args.data)
spec["data"] = _prepare_data(data, data_transformers)
return VegaLite(spec)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
from __future__ import annotations
CONST_LISTING = {
"NaN": "not a number (same as JavaScript literal NaN)",
"LN10": "the natural log of 10 (alias to Math.LN10)",
"E": "the transcendental number e (alias to Math.E)",
"LOG10E": "the base 10 logarithm e (alias to Math.LOG10E)",
"LOG2E": "the base 2 logarithm of e (alias to Math.LOG2E)",
"SQRT1_2": "the square root of 0.5 (alias to Math.SQRT1_2)",
"LN2": "the natural log of 2 (alias to Math.LN2)",
"SQRT2": "the square root of 2 (alias to Math.SQRT1_2)",
"PI": "the transcendental number pi (alias to Math.PI)",
}

View File

@@ -0,0 +1,282 @@
from __future__ import annotations
import datetime as dt
from typing import TYPE_CHECKING, Any, Literal, Union
from altair.utils import SchemaBase
if TYPE_CHECKING:
import sys
from altair.vegalite.v5.schema._typing import Map, PrimitiveValue_T
if sys.version_info >= (3, 10):
from typing import TypeAlias
else:
from typing_extensions import TypeAlias
class DatumType:
"""An object to assist in building Vega-Lite Expressions."""
def __repr__(self) -> str:
return "datum"
def __getattr__(self, attr) -> GetAttrExpression:
if attr.startswith("__") and attr.endswith("__"):
raise AttributeError(attr)
return GetAttrExpression("datum", attr)
def __getitem__(self, attr) -> GetItemExpression:
return GetItemExpression("datum", attr)
def __call__(self, datum, **kwargs) -> dict[str, Any]:
"""Specify a datum for use in an encoding."""
return dict(datum=datum, **kwargs)
datum = DatumType()
def _js_repr(val) -> str:
"""Return a javascript-safe string representation of val."""
if val is True:
return "true"
elif val is False:
return "false"
elif val is None:
return "null"
elif isinstance(val, OperatorMixin):
return val._to_expr()
elif isinstance(val, dt.date):
return _from_date_datetime(val)
else:
return repr(val)
def _from_date_datetime(obj: dt.date | dt.datetime, /) -> str:
"""
Parse native `datetime.(date|datetime)` into a `datetime expression`_ string.
**Month is 0-based**
.. _datetime expression:
https://vega.github.io/vega/docs/expressions/#datetime
"""
fn_name: Literal["datetime", "utc"] = "datetime"
args: tuple[int, ...] = obj.year, obj.month - 1, obj.day
if isinstance(obj, dt.datetime):
if tzinfo := obj.tzinfo:
if tzinfo is dt.timezone.utc:
fn_name = "utc"
else:
msg = (
f"Unsupported timezone {tzinfo!r}.\n"
"Only `'UTC'` or naive (local) datetimes are permitted.\n"
"See https://altair-viz.github.io/user_guide/generated/core/altair.DateTime.html"
)
raise TypeError(msg)
us = obj.microsecond
ms = us if us == 0 else us // 1_000
args = *args, obj.hour, obj.minute, obj.second, ms
return FunctionExpression(fn_name, args)._to_expr()
# Designed to work with Expression and VariableParameter
class OperatorMixin:
def _to_expr(self) -> str:
return repr(self)
def _from_expr(self, expr) -> Any:
return expr
def __add__(self, other):
comp_value = BinaryExpression("+", self, other)
return self._from_expr(comp_value)
def __radd__(self, other):
comp_value = BinaryExpression("+", other, self)
return self._from_expr(comp_value)
def __sub__(self, other):
comp_value = BinaryExpression("-", self, other)
return self._from_expr(comp_value)
def __rsub__(self, other):
comp_value = BinaryExpression("-", other, self)
return self._from_expr(comp_value)
def __mul__(self, other):
comp_value = BinaryExpression("*", self, other)
return self._from_expr(comp_value)
def __rmul__(self, other):
comp_value = BinaryExpression("*", other, self)
return self._from_expr(comp_value)
def __truediv__(self, other):
comp_value = BinaryExpression("/", self, other)
return self._from_expr(comp_value)
def __rtruediv__(self, other):
comp_value = BinaryExpression("/", other, self)
return self._from_expr(comp_value)
__div__ = __truediv__
__rdiv__ = __rtruediv__
def __mod__(self, other):
comp_value = BinaryExpression("%", self, other)
return self._from_expr(comp_value)
def __rmod__(self, other):
comp_value = BinaryExpression("%", other, self)
return self._from_expr(comp_value)
def __pow__(self, other):
# "**" Javascript operator is not supported in all browsers
comp_value = FunctionExpression("pow", (self, other))
return self._from_expr(comp_value)
def __rpow__(self, other):
# "**" Javascript operator is not supported in all browsers
comp_value = FunctionExpression("pow", (other, self))
return self._from_expr(comp_value)
def __neg__(self):
comp_value = UnaryExpression("-", self)
return self._from_expr(comp_value)
def __pos__(self):
comp_value = UnaryExpression("+", self)
return self._from_expr(comp_value)
# comparison operators
def __eq__(self, other):
comp_value = BinaryExpression("===", self, other)
return self._from_expr(comp_value)
def __ne__(self, other):
comp_value = BinaryExpression("!==", self, other)
return self._from_expr(comp_value)
def __gt__(self, other):
comp_value = BinaryExpression(">", self, other)
return self._from_expr(comp_value)
def __lt__(self, other):
comp_value = BinaryExpression("<", self, other)
return self._from_expr(comp_value)
def __ge__(self, other):
comp_value = BinaryExpression(">=", self, other)
return self._from_expr(comp_value)
def __le__(self, other):
comp_value = BinaryExpression("<=", self, other)
return self._from_expr(comp_value)
def __abs__(self):
comp_value = FunctionExpression("abs", (self,))
return self._from_expr(comp_value)
# logical operators
def __and__(self, other):
comp_value = BinaryExpression("&&", self, other)
return self._from_expr(comp_value)
def __rand__(self, other):
comp_value = BinaryExpression("&&", other, self)
return self._from_expr(comp_value)
def __or__(self, other):
comp_value = BinaryExpression("||", self, other)
return self._from_expr(comp_value)
def __ror__(self, other):
comp_value = BinaryExpression("||", other, self)
return self._from_expr(comp_value)
def __invert__(self):
comp_value = UnaryExpression("!", self)
return self._from_expr(comp_value)
class Expression(OperatorMixin, SchemaBase):
"""
Expression.
Base object for enabling build-up of Javascript expressions using
a Python syntax. Calling ``repr(obj)`` will return a Javascript
representation of the object and the operations it encodes.
"""
_schema = {"type": "string"}
def to_dict(self, *args, **kwargs):
return repr(self)
def __setattr__(self, attr, val) -> None:
# We don't need the setattr magic defined in SchemaBase
return object.__setattr__(self, attr, val)
# item access
def __getitem__(self, val):
return GetItemExpression(self, val)
class UnaryExpression(Expression):
def __init__(self, op, val) -> None:
super().__init__(op=op, val=val)
def __repr__(self):
return f"({self.op}{_js_repr(self.val)})"
class BinaryExpression(Expression):
def __init__(self, op, lhs, rhs) -> None:
super().__init__(op=op, lhs=lhs, rhs=rhs)
def __repr__(self):
return f"({_js_repr(self.lhs)} {self.op} {_js_repr(self.rhs)})"
class FunctionExpression(Expression):
def __init__(self, name, args) -> None:
super().__init__(name=name, args=args)
def __repr__(self):
args = ",".join(_js_repr(arg) for arg in self.args)
return f"{self.name}({args})"
class ConstExpression(Expression):
def __init__(self, name) -> None:
super().__init__(name=name)
def __repr__(self) -> str:
return str(self.name)
class GetAttrExpression(Expression):
def __init__(self, group, name) -> None:
super().__init__(group=group, name=name)
def __repr__(self):
return f"{self.group}.{self.name}"
class GetItemExpression(Expression):
def __init__(self, group, name) -> None:
super().__init__(group=group, name=name)
def __repr__(self) -> str:
return f"{self.group}[{self.name!r}]"
IntoExpression: TypeAlias = Union[
"PrimitiveValue_T", dt.date, dt.datetime, OperatorMixin, "Map"
]

View File

@@ -0,0 +1,167 @@
from __future__ import annotations
FUNCTION_LISTING = {
"isArray": r"Returns true if _value_ is an array, false otherwise.",
"isBoolean": r"Returns true if _value_ is a boolean (`true` or `false`), false otherwise.",
"isDate": r"Returns true if _value_ is a Date object, false otherwise. This method will return false for timestamp numbers or date-formatted strings; it recognizes Date objects only.",
"isDefined": r"Returns true if _value_ is a defined value, false if _value_ equals `undefined`. This method will return true for `null` and `NaN` values.",
"isNumber": r"Returns true if _value_ is a number, false otherwise. `NaN` and `Infinity` are considered numbers.",
"isObject": r"Returns true if _value_ is an object (including arrays and Dates), false otherwise.",
"isRegExp": r"Returns true if _value_ is a RegExp (regular expression) object, false otherwise.",
"isString": r"Returns true if _value_ is a string, false otherwise.",
"isValid": r"Returns true if _value_ is not `null`, `undefined`, or `NaN`, false otherwise.",
"toBoolean": r"Coerces the input _value_ to a string. Null values and empty strings are mapped to `null`.",
"toDate": r"Coerces the input _value_ to a Date instance. Null values and empty strings are mapped to `null`. If an optional _parser_ function is provided, it is used to perform date parsing, otherwise `Date.parse` is used. Be aware that `Date.parse` has different implementations across browsers!",
"toNumber": r"Coerces the input _value_ to a number. Null values and empty strings are mapped to `null`.",
"toString": r"Coerces the input _value_ to a string. Null values and empty strings are mapped to `null`.",
"if": r"If _test_ is truthy, returns _thenValue_. Otherwise, returns _elseValue_. The _if_ function is equivalent to the ternary operator `a ? b : c`.",
"isNaN": r"Returns true if _value_ is not a number. Same as JavaScript's `isNaN`.",
"isFinite": r"Returns true if _value_ is a finite number. Same as JavaScript's `isFinite`.",
"abs": r"Returns the absolute value of _value_. Same as JavaScript's `Math.abs`.",
"acos": r"Trigonometric arccosine. Same as JavaScript's `Math.acos`.",
"asin": r"Trigonometric arcsine. Same as JavaScript's `Math.asin`.",
"atan": r"Trigonometric arctangent. Same as JavaScript's `Math.atan`.",
"atan2": r"Returns the arctangent of _dy / dx_. Same as JavaScript's `Math.atan2`.",
"ceil": r"Rounds _value_ to the nearest integer of equal or greater value. Same as JavaScript's `Math.ceil`.",
"clamp": r"Restricts _value_ to be between the specified _min_ and _max_.",
"cos": r"Trigonometric cosine. Same as JavaScript's `Math.cos`.",
"exp": r"Returns the value of _e_ raised to the provided _exponent_. Same as JavaScript's `Math.exp`.",
"floor": r"Rounds _value_ to the nearest integer of equal or lower value. Same as JavaScript's `Math.floor`.",
"hypot": r"Returns the square root of the sum of squares of its arguments. Same as JavaScript's `Math.hypot`.",
"log": r"Returns the natural logarithm of _value_. Same as JavaScript's `Math.log`.",
"max": r"Returns the maximum argument value. Same as JavaScript's `Math.max`.",
"min": r"Returns the minimum argument value. Same as JavaScript's `Math.min`.",
"pow": r"Returns _value_ raised to the given _exponent_. Same as JavaScript's `Math.pow`.",
"random": r"Returns a pseudo-random number in the range [0,1). Same as JavaScript's `Math.random`.",
"round": r"Rounds _value_ to the nearest integer. Same as JavaScript's `Math.round`.",
"sin": r"Trigonometric sine. Same as JavaScript's `Math.sin`.",
"sqrt": r"Square root function. Same as JavaScript's `Math.sqrt`.",
"tan": r"Trigonometric tangent. Same as JavaScript's `Math.tan`.",
"sampleNormal": r"Returns a sample from a univariate [normal (Gaussian) probability distribution](https://en.wikipedia.org/wiki/Normal_distribution) with specified _mean_ and standard deviation _stdev_. If unspecified, the mean defaults to `0` and the standard deviation defaults to `1`.",
"cumulativeNormal": r"Returns the value of the [cumulative distribution function](https://en.wikipedia.org/wiki/Cumulative_distribution_function) at the given input domain _value_ for a normal distribution with specified _mean_ and standard deviation _stdev_. If unspecified, the mean defaults to `0` and the standard deviation defaults to `1`.",
"densityNormal": r"Returns the value of the [probability density function](https://en.wikipedia.org/wiki/Probability_density_function) at the given input domain _value_, for a normal distribution with specified _mean_ and standard deviation _stdev_. If unspecified, the mean defaults to `0` and the standard deviation defaults to `1`.",
"quantileNormal": r"Returns the quantile value (the inverse of the [cumulative distribution function](https://en.wikipedia.org/wiki/Cumulative_distribution_function)) for the given input _probability_, for a normal distribution with specified _mean_ and standard deviation _stdev_. If unspecified, the mean defaults to `0` and the standard deviation defaults to `1`.",
"sampleLogNormal": r"Returns a sample from a univariate [log-normal probability distribution](https://en.wikipedia.org/wiki/Log-normal_distribution) with specified log _mean_ and log standard deviation _stdev_. If unspecified, the log mean defaults to `0` and the log standard deviation defaults to `1`.",
"cumulativeLogNormal": r"Returns the value of the [cumulative distribution function](https://en.wikipedia.org/wiki/Cumulative_distribution_function) at the given input domain _value_ for a log-normal distribution with specified log _mean_ and log standard deviation _stdev_. If unspecified, the log mean defaults to `0` and the log standard deviation defaults to `1`.",
"densityLogNormal": r"Returns the value of the [probability density function](https://en.wikipedia.org/wiki/Probability_density_function) at the given input domain _value_, for a log-normal distribution with specified log _mean_ and log standard deviation _stdev_. If unspecified, the log mean defaults to `0` and the log standard deviation defaults to `1`.",
"quantileLogNormal": r"Returns the quantile value (the inverse of the [cumulative distribution function](https://en.wikipedia.org/wiki/Cumulative_distribution_function)) for the given input _probability_, for a log-normal distribution with specified log _mean_ and log standard deviation _stdev_. If unspecified, the log mean defaults to `0` and the log standard deviation defaults to `1`.",
"sampleUniform": r"Returns a sample from a univariate [continuous uniform probability distribution](https://en.wikipedia.org/wiki/Uniform_distribution_(continuous)) over the interval [_min_, _max_). If unspecified, _min_ defaults to `0` and _max_ defaults to `1`. If only one argument is provided, it is interpreted as the _max_ value.",
"cumulativeUniform": r"Returns the value of the [cumulative distribution function](https://en.wikipedia.org/wiki/Cumulative_distribution_function) at the given input domain _value_ for a uniform distribution over the interval [_min_, _max_). If unspecified, _min_ defaults to `0` and _max_ defaults to `1`. If only one argument is provided, it is interpreted as the _max_ value.",
"densityUniform": r"Returns the value of the [probability density function](https://en.wikipedia.org/wiki/Probability_density_function) at the given input domain _value_, for a uniform distribution over the interval [_min_, _max_). If unspecified, _min_ defaults to `0` and _max_ defaults to `1`. If only one argument is provided, it is interpreted as the _max_ value.",
"quantileUniform": r"Returns the quantile value (the inverse of the [cumulative distribution function](https://en.wikipedia.org/wiki/Cumulative_distribution_function)) for the given input _probability_, for a uniform distribution over the interval [_min_, _max_). If unspecified, _min_ defaults to `0` and _max_ defaults to `1`. If only one argument is provided, it is interpreted as the _max_ value.",
"now": r"Returns the timestamp for the current time.",
"datetime": r"Returns a new `Date` instance. The _month_ is 0-based, such that `1` represents February.",
"date": r"Returns the day of the month for the given _datetime_ value, in local time.",
"day": r"Returns the day of the week for the given _datetime_ value, in local time.",
"dayofyear": r"Returns the one-based day of the year for the given _datetime_ value, in local time.",
"year": r"Returns the year for the given _datetime_ value, in local time.",
"quarter": r"Returns the quarter of the year (0-3) for the given _datetime_ value, in local time.",
"month": r"Returns the (zero-based) month for the given _datetime_ value, in local time.",
"week": r"Returns the week number of the year for the given _datetime_, in local time. This function assumes Sunday-based weeks. Days before the first Sunday of the year are considered to be in week 0, the first Sunday of the year is the start of week 1, the second Sunday week 2, _etc._.",
"hours": r"Returns the hours component for the given _datetime_ value, in local time.",
"minutes": r"Returns the minutes component for the given _datetime_ value, in local time.",
"seconds": r"Returns the seconds component for the given _datetime_ value, in local time.",
"milliseconds": r"Returns the milliseconds component for the given _datetime_ value, in local time.",
"time": r"Returns the epoch-based timestamp for the given _datetime_ value.",
"timezoneoffset": r"Returns the timezone offset from the local timezone to UTC for the given _datetime_ value.",
"timeOffset": r"Returns a new `Date` instance that offsets the given _date_ by the specified time [_unit_](../api/time/#time-units) in the local timezone. The optional _step_ argument indicates the number of time unit steps to offset by (default 1).",
"timeSequence": r"Returns an array of `Date` instances from _start_ (inclusive) to _stop_ (exclusive), with each entry separated by the given time [_unit_](../api/time/#time-units) in the local timezone. The optional _step_ argument indicates the number of time unit steps to take between each sequence entry (default 1).",
"utc": r"Returns a timestamp for the given UTC date. The _month_ is 0-based, such that `1` represents February.",
"utcdate": r"Returns the day of the month for the given _datetime_ value, in UTC time.",
"utcday": r"Returns the day of the week for the given _datetime_ value, in UTC time.",
"utcdayofyear": r"Returns the one-based day of the year for the given _datetime_ value, in UTC time.",
"utcyear": r"Returns the year for the given _datetime_ value, in UTC time.",
"utcquarter": r"Returns the quarter of the year (0-3) for the given _datetime_ value, in UTC time.",
"utcmonth": r"Returns the (zero-based) month for the given _datetime_ value, in UTC time.",
"utcweek": r"Returns the week number of the year for the given _datetime_, in UTC time. This function assumes Sunday-based weeks. Days before the first Sunday of the year are considered to be in week 0, the first Sunday of the year is the start of week 1, the second Sunday week 2, _etc._.",
"utchours": r"Returns the hours component for the given _datetime_ value, in UTC time.",
"utcminutes": r"Returns the minutes component for the given _datetime_ value, in UTC time.",
"utcseconds": r"Returns the seconds component for the given _datetime_ value, in UTC time.",
"utcmilliseconds": r"Returns the milliseconds component for the given _datetime_ value, in UTC time.",
"utcOffset": r"Returns a new `Date` instance that offsets the given _date_ by the specified time [_unit_](../api/time/#time-units) in UTC time. The optional _step_ argument indicates the number of time unit steps to offset by (default 1).",
"utcSequence": r"Returns an array of `Date` instances from _start_ (inclusive) to _stop_ (exclusive), with each entry separated by the given time [_unit_](../api/time/#time-units) in UTC time. The optional _step_ argument indicates the number of time unit steps to take between each sequence entry (default 1).",
"extent": r"Returns a new _[min, max]_ array with the minimum and maximum values of the input array, ignoring `null`, `undefined`, and `NaN` values.",
"clampRange": r"Clamps a two-element _range_ array in a span-preserving manner. If the span of the input _range_ is less than _(max - min)_ and an endpoint exceeds either the _min_ or _max_ value, the range is translated such that the span is preserved and one endpoint touches the boundary of the _[min, max]_ range. If the span exceeds _(max - min)_, the range _[min, max]_ is returned.",
"indexof": r"Returns the first index of _value_ in the input _array_, or the first index of _substring_ in the input _string_..",
"inrange": r"Tests whether _value_ lies within (or is equal to either) the first and last values of the _range_ array.",
"join": r"Returns a new string by concatenating all of the elements of the input _array_, separated by commas or a specified _separator_ string.",
"lastindexof": r"Returns the last index of _value_ in the input _array_, or the last index of _substring_ in the input _string_..",
"length": r"Returns the length of the input _array_, or the length of the input _string_.",
"lerp": r"Returns the linearly interpolated value between the first and last entries in the _array_ for the provided interpolation _fraction_ (typically between 0 and 1). For example, `lerp([0, 50], 0.5)` returns 25.",
"peek": r"Returns the last element in the input _array_. Similar to the built-in `Array.pop` method, except that it does not remove the last element. This method is a convenient shorthand for `array[array.length - 1]`.",
"pluck": r"Retrieves the value for the specified *field* from a given *array* of objects. The input *field* string may include nested properties (e.g., `foo.bar.bz`).",
"reverse": r"Returns a new array with elements in a reverse order of the input _array_. The first array element becomes the last, and the last array element becomes the first.",
"sequence": r"Returns an array containing an arithmetic sequence of numbers. If _step_ is omitted, it defaults to 1. If _start_ is omitted, it defaults to 0. The _stop_ value is exclusive; it is not included in the result. If _step_ is positive, the last element is the largest _start + i * step_ less than _stop_; if _step_ is negative, the last element is the smallest _start + i * step_ greater than _stop_. If the returned array would contain an infinite number of values, an empty range is returned. The arguments are not required to be integers.",
"slice": r"Returns a section of _array_ between the _start_ and _end_ indices. If the _end_ argument is negative, it is treated as an offset from the end of the array (_length(array) + end_).",
"span": r"Returns the span of _array_: the difference between the last and first elements, or _array[array.length-1] - array[0]_. Or if input is a string: a section of _string_ between the _start_ and _end_ indices. If the _end_ argument is negative, it is treated as an offset from the end of the string (_length(string) + end_)..",
"lower": r"Transforms _string_ to lower-case letters.",
"pad": r"Pads a _string_ value with repeated instances of a _character_ up to a specified _length_. If _character_ is not specified, a space (' ') is used. By default, padding is added to the end of a string. An optional _align_ parameter specifies if padding should be added to the `'left'` (beginning), `'center'`, or `'right'` (end) of the input string.",
"parseFloat": r"Parses the input _string_ to a floating-point value. Same as JavaScript's `parseFloat`.",
"parseInt": r"Parses the input _string_ to an integer value. Same as JavaScript's `parseInt`.",
"replace": r"Returns a new string with some or all matches of _pattern_ replaced by a _replacement_ string. The _pattern_ can be a string or a regular expression. If _pattern_ is a string, only the first instance will be replaced. Same as [JavaScript's String.replace](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace).",
"split": r"Returns an array of tokens created by splitting the input _string_ according to a provided _separator_ pattern. The result can optionally be constrained to return at most _limit_ tokens.",
"substring": r"Returns a section of _string_ between the _start_ and _end_ indices.",
"trim": r"Returns a trimmed string with preceding and trailing whitespace removed.",
"truncate": r"Truncates an input _string_ to a target _length_. The optional _align_ argument indicates what part of the string should be truncated: `'left'` (the beginning), `'center'`, or `'right'` (the end). By default, the `'right'` end of the string is truncated. The optional _ellipsis_ argument indicates the string to use to indicate truncated content; by default the ellipsis character `...` (`\\u2026`) is used.",
"upper": r"Transforms _string_ to upper-case letters.",
"merge": r"Merges the input objects _object1_, _object2_, etc into a new output object. Inputs are visited in sequential order, such that key values from later arguments can overwrite those from earlier arguments. Example: `merge({a:1, b:2}, {a:3}) -> {a:3, b:2}`.",
"dayFormat": r"Formats a (0-6) _weekday_ number as a full week day name, according to the current locale. For example: `dayFormat(0) -> \"Sunday\"`.",
"dayAbbrevFormat": r"Formats a (0-6) _weekday_ number as an abbreviated week day name, according to the current locale. For example: `dayAbbrevFormat(0) -> \"Sun\"`.",
"format": r"Formats a numeric _value_ as a string. The _specifier_ must be a valid [d3-format specifier](https://github.com/d3/d3-format/) (e.g., `format(value, ',.2f')`.",
"monthFormat": r"Formats a (zero-based) _month_ number as a full month name, according to the current locale. For example: `monthFormat(0) -> \"January\"`.",
"monthAbbrevFormat": r"Formats a (zero-based) _month_ number as an abbreviated month name, according to the current locale. For example: `monthAbbrevFormat(0) -> \"Jan\"`.",
"timeUnitSpecifier": r"Returns a time format specifier string for the given time [_units_](../api/time/#time-units). The optional _specifiers_ object provides a set of specifier sub-strings for customizing the format; for more, see the [timeUnitSpecifier API documentation](../api/time/#timeUnitSpecifier). The resulting specifier string can then be used as input to the [timeFormat](#timeFormat) or [utcFormat](#utcFormat) functions, or as the _format_ parameter of an axis or legend. For example: `timeFormat(date, timeUnitSpecifier('year'))` or `timeFormat(date, timeUnitSpecifier(['hours', 'minutes']))`.",
"timeFormat": r"Formats a datetime _value_ (either a `Date` object or timestamp) as a string, according to the local time. The _specifier_ must be a valid [d3-time-format specifier](https://github.com/d3/d3-time-format/). For example: `timeFormat(timestamp, '%A')`.",
"timeParse": r"Parses a _string_ value to a Date object, according to the local time. The _specifier_ must be a valid [d3-time-format specifier](https://github.com/d3/d3-time-format/). For example: `timeParse('June 30, 2015', '%B %d, %Y')`.",
"utcFormat": r"Formats a datetime _value_ (either a `Date` object or timestamp) as a string, according to [UTC](https://en.wikipedia.org/wiki/Coordinated_Universal_Time) time. The _specifier_ must be a valid [d3-time-format specifier](https://github.com/d3/d3-time-format/). For example: `utcFormat(timestamp, '%A')`.",
"utcParse": r"Parses a _string_ value to a Date object, according to [UTC](https://en.wikipedia.org/wiki/Coordinated_Universal_Time) time. The _specifier_ must be a valid [d3-time-format specifier](https://github.com/d3/d3-time-format/). For example: `utcParse('June 30, 2015', '%B %d, %Y')`.",
"regexp": r"Creates a regular expression instance from an input _pattern_ string and optional _flags_. Same as [JavaScript's `RegExp`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp).",
"test": r"Evaluates a regular expression _regexp_ against the input _string_, returning `true` if the string matches the pattern, `false` otherwise. For example: `test(/\\d{3}/, \"32-21-9483\") -> true`.",
"rgb": r"Constructs a new [RGB](https://en.wikipedia.org/wiki/RGB_color_model) color. If _r_, _g_ and _b_ are specified, these represent the channel values of the returned color; an _opacity_ may also be specified. If a CSS Color Module Level 3 _specifier_ string is specified, it is parsed and then converted to the RGB color space. Uses [d3-color's rgb function](https://github.com/d3/d3-color#rgb).",
"hsl": r"Constructs a new [HSL](https://en.wikipedia.org/wiki/HSL_and_HSV) color. If _h_, _s_ and _l_ are specified, these represent the channel values of the returned color; an _opacity_ may also be specified. If a CSS Color Module Level 3 _specifier_ string is specified, it is parsed and then converted to the HSL color space. Uses [d3-color's hsl function](https://github.com/d3/d3-color#hsl).",
"lab": r"Constructs a new [CIE LAB](https://en.wikipedia.org/wiki/Lab_color_space#CIELAB) color. If _l_, _a_ and _b_ are specified, these represent the channel values of the returned color; an _opacity_ may also be specified. If a CSS Color Module Level 3 _specifier_ string is specified, it is parsed and then converted to the LAB color space. Uses [d3-color's lab function](https://github.com/d3/d3-color#lab).",
"hcl": r"Constructs a new [HCL](https://en.wikipedia.org/wiki/Lab_color_space#CIELAB) (hue, chroma, luminance) color. If _h_, _c_ and _l_ are specified, these represent the channel values of the returned color; an _opacity_ may also be specified. If a CSS Color Module Level 3 _specifier_ string is specified, it is parsed and then converted to the HCL color space. Uses [d3-color's hcl function](https://github.com/d3/d3-color#hcl).",
"luminance": r"Returns the luminance for the given color _specifier_ (compatible with [d3-color's rgb function](https://github.com/d3/d3-color#rgb)). The luminance is calculated according to the [W3C Web Content Accessibility Guidelines](https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef).",
"contrast": r"Returns the contrast ratio between the input color specifiers as a float between 1 and 21. The contrast is calculated according to the [W3C Web Content Accessibility Guidelines](https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef).",
"item": r"Returns the current scenegraph item that is the target of the event.",
"group": r"Returns the scenegraph group mark item in which the current event has occurred. If no arguments are provided, the immediate parent group is returned. If a group name is provided, the matching ancestor group item is returned.",
"xy": r"Returns the x- and y-coordinates for the current event as a two-element array. If no arguments are provided, the top-level coordinate space of the view is used. If a scenegraph _item_ (or string group name) is provided, the coordinate space of the group item is used.",
"x": r"Returns the x coordinate for the current event. If no arguments are provided, the top-level coordinate space of the view is used. If a scenegraph _item_ (or string group name) is provided, the coordinate space of the group item is used.",
"y": r"Returns the y coordinate for the current event. If no arguments are provided, the top-level coordinate space of the view is used. If a scenegraph _item_ (or string group name) is provided, the coordinate space of the group item is used.",
"pinchDistance": r"Returns the pixel distance between the first two touch points of a multi-touch event.",
"pinchAngle": r"Returns the angle of the line connecting the first two touch points of a multi-touch event.",
"inScope": r"Returns true if the given scenegraph _item_ is a descendant of the group mark in which the event handler was defined, false otherwise.",
"data": r"Returns the array of data objects for the Vega data set with the given _name_. If the data set is not found, returns an empty array.",
"indata": r"Tests if the data set with a given _name_ contains a datum with a _field_ value that matches the input _value_. For example: `indata('table', 'category', value)`.",
"scale": r"Applies the named scale transform (or projection) to the specified _value_. The optional _group_ argument takes a scenegraph group mark item to indicate the specific scope in which to look up the scale or projection.",
"invert": r"Inverts the named scale transform (or projection) for the specified _value_. The optional _group_ argument takes a scenegraph group mark item to indicate the specific scope in which to look up the scale or projection.",
"copy": r"Returns a copy (a new cloned instance) of the named scale transform of projection, or `undefined` if no scale or projection is found. The optional _group_ argument takes a scenegraph group mark item to indicate the specific scope in which to look up the scale or projection.",
"domain": r"Returns the scale domain array for the named scale transform, or an empty array if the scale is not found. The optional _group_ argument takes a scenegraph group mark item to indicate the specific scope in which to look up the scale.",
"range": r"Returns the scale range array for the named scale transform, or an empty array if the scale is not found. The optional _group_ argument takes a scenegraph group mark item to indicate the specific scope in which to look up the scale.",
"bandwidth": r"Returns the current band width for the named band scale transform, or zero if the scale is not found or is not a band scale. The optional _group_ argument takes a scenegraph group mark item to indicate the specific scope in which to look up the scale.",
"bandspace": r"Returns the number of steps needed within a band scale, based on the _count_ of domain elements and the inner and outer padding values. While normally calculated within the scale itself, this function can be helpful for determining the size of a chart's layout.",
"gradient": r"Returns a linear color gradient for the _scale_ (whose range must be a [continuous color scheme](../schemes)) and starting and ending points _p0_ and _p1_, each an _[x, y]_ array. The points _p0_ and _p1_ should be expressed in normalized coordinates in the domain [0, 1], relative to the bounds of the item being colored. If unspecified, _p0_ defaults to `[0, 0]` and _p1_ defaults to `[1, 0]`, for a horizontal gradient that spans the full bounds of an item. The optional _count_ argument indicates a desired target number of sample points to take from the color scale.",
"panLinear": r"Given a linear scale _domain_ array with numeric or datetime values, returns a new two-element domain array that is the result of panning the domain by a fractional _delta_. The _delta_ value represents fractional units of the scale range; for example, `0.5` indicates panning the scale domain to the right by half the scale range.",
"panLog": r"Given a log scale _domain_ array with numeric or datetime values, returns a new two-element domain array that is the result of panning the domain by a fractional _delta_. The _delta_ value represents fractional units of the scale range; for example, `0.5` indicates panning the scale domain to the right by half the scale range.",
"panPow": r"Given a power scale _domain_ array with numeric or datetime values and the given _exponent_, returns a new two-element domain array that is the result of panning the domain by a fractional _delta_. The _delta_ value represents fractional units of the scale range; for example, `0.5` indicates panning the scale domain to the right by half the scale range.",
"panSymlog": r"Given a symmetric log scale _domain_ array with numeric or datetime values parameterized by the given _constant_, returns a new two-element domain array that is the result of panning the domain by a fractional _delta_. The _delta_ value represents fractional units of the scale range; for example, `0.5` indicates panning the scale domain to the right by half the scale range.",
"zoomLinear": r"Given a linear scale _domain_ array with numeric or datetime values, returns a new two-element domain array that is the result of zooming the domain by a _scaleFactor_, centered at the provided fractional _anchor_. The _anchor_ value represents the zoom position in terms of fractional units of the scale range; for example, `0.5` indicates a zoom centered on the mid-point of the scale range.",
"zoomLog": r"Given a log scale _domain_ array with numeric or datetime values, returns a new two-element domain array that is the result of zooming the domain by a _scaleFactor_, centered at the provided fractional _anchor_. The _anchor_ value represents the zoom position in terms of fractional units of the scale range; for example, `0.5` indicates a zoom centered on the mid-point of the scale range.",
"zoomPow": r"Given a power scale _domain_ array with numeric or datetime values and the given _exponent_, returns a new two-element domain array that is the result of zooming the domain by a _scaleFactor_, centered at the provided fractional _anchor_. The _anchor_ value represents the zoom position in terms of fractional units of the scale range; for example, `0.5` indicates a zoom centered on the mid-point of the scale range.",
"zoomSymlog": r"Given a symmetric log scale _domain_ array with numeric or datetime values parameterized by the given _constant_, returns a new two-element domain array that is the result of zooming the domain by a _scaleFactor_, centered at the provided fractional _anchor_. The _anchor_ value represents the zoom position in terms of fractional units of the scale range; for example, `0.5` indicates a zoom centered on the mid-point of the scale range.",
"geoArea": r"Returns the projected planar area (typically in square pixels) of a GeoJSON _feature_ according to the named _projection_. If the _projection_ argument is `null`, computes the spherical area in steradians using unprojected longitude, latitude coordinates. The optional _group_ argument takes a scenegraph group mark item to indicate the specific scope in which to look up the projection. Uses d3-geo's [geoArea](https://github.com/d3/d3-geo#geoArea) and [path.area](https://github.com/d3/d3-geo#path_area) methods.",
"geoBounds": r"Returns the projected planar bounding box (typically in pixels) for the specified GeoJSON _feature_, according to the named _projection_. The bounding box is represented by a two-dimensional array: [[_x0_, _y0_], [_x1_, _y1_]], where _x0_ is the minimum x-coordinate, _y0_ is the minimum y-coordinate, _x1_ is the maximum x-coordinate, and _y1_ is the maximum y-coordinate. If the _projection_ argument is `null`, computes the spherical bounding box using unprojected longitude, latitude coordinates. The optional _group_ argument takes a scenegraph group mark item to indicate the specific scope in which to look up the projection. Uses d3-geo's [geoBounds](https://github.com/d3/d3-geo#geoBounds) and [path.bounds](https://github.com/d3/d3-geo#path_bounds) methods.",
"geoCentroid": r"Returns the projected planar centroid (typically in pixels) for the specified GeoJSON _feature_, according to the named _projection_. If the _projection_ argument is `null`, computes the spherical centroid using unprojected longitude, latitude coordinates. The optional _group_ argument takes a scenegraph group mark item to indicate the specific scope in which to look up the projection. Uses d3-geo's [geoCentroid](https://github.com/d3/d3-geo#geoCentroid) and [path.centroid](https://github.com/d3/d3-geo#path_centroid) methods.",
"treePath": r"For the hierarchy data set with the given _name_, returns the shortest path through from the _source_ node id to the _target_ node id. The path starts at the _source_ node, ascends to the least common ancestor of the _source_ node and the _target_ node, and then descends to the _target_ node.",
"treeAncestors": r"For the hierarchy data set with the given _name_, returns the array of ancestors nodes, starting with the input _node_, then followed by each parent up to the root.",
"containerSize": r"Returns the current CSS box size (`[el.clientWidth, el.clientHeight]`) of the parent DOM element that contains the Vega view. If there is no container element, returns `[undefined, undefined]`.",
"screen": r"Returns the [`window.screen`](https://developer.mozilla.org/en-US/docs/Web/API/Window/screen) object, or `{}` if Vega is not running in a browser environment.",
"windowSize": r"Returns the current window size (`[window.innerWidth, window.innerHeight]`) or `[undefined, undefined]` if Vega is not running in a browser environment.",
"warn": r"Logs a warning message and returns the last argument. For the message to appear in the console, the visualization view must have the appropriate logging level set.",
"info": r"Logs an informative message and returns the last argument. For the message to appear in the console, the visualization view must have the appropriate logging level set.",
"debug": r"Logs a debugging message and returns the last argument. For the message to appear in the console, the visualization view must have the appropriate logging level set.",
}
# This maps vega expression function names to the Python name
NAME_MAP = {"if": "if_"}

View File

@@ -0,0 +1,21 @@
try:
import anywidget # noqa: F401
except ImportError:
# When anywidget isn't available, create stand-in JupyterChart class
# that raises an informative import error on construction. This
# way we can make JupyterChart available in the altair namespace
# when anywidget is not installed
class JupyterChart:
def __init__(self, *args, **kwargs):
msg = (
"The Altair JupyterChart requires the anywidget \n"
"Python package which may be installed using pip with\n"
" pip install anywidget\n"
"or using conda with\n"
" conda install -c conda-forge anywidget\n"
"Afterwards, you will need to restart your Python kernel."
)
raise ImportError(msg)
else:
from .jupyter_chart import JupyterChart # noqa: F401

View File

@@ -0,0 +1,2 @@
# JupyterChart
This directory contains the JavaScript portion of the Altair `JupyterChart`. The `JupyterChart` is based on the [AnyWidget](https://anywidget.dev/) project.

View File

@@ -0,0 +1,230 @@
import vegaEmbed from "https://esm.sh/vega-embed@6?deps=vega@5&deps=vega-lite@5.20.1";
import lodashDebounce from "https://esm.sh/lodash-es@4.17.21/debounce";
// Note: For offline support, the import lines above are removed and the remaining script
// is bundled using vl-convert's javascript_bundle function. See the documentation of
// the javascript_bundle function for details on the available imports and their names.
// If an additional import is required in the future, it will need to be added to vl-convert
// in order to preserve offline support.
async function render({ model, el }) {
let finalize;
function showError(error){
el.innerHTML = (
'<div style="color:red;">'
+ '<p>JavaScript Error: ' + error.message + '</p>'
+ "<p>This usually means there's a typo in your chart specification. "
+ "See the javascript console for the full traceback.</p>"
+ '</div>'
);
}
const reembed = async () => {
if (finalize != null) {
finalize();
}
model.set("local_tz", Intl.DateTimeFormat().resolvedOptions().timeZone);
let spec = structuredClone(model.get("spec"));
if (spec == null) {
// Remove any existing chart and return
while (el.firstChild) {
el.removeChild(el.lastChild);
}
model.save_changes();
return;
}
let embedOptions = structuredClone(model.get("embed_options")) ?? undefined;
let api;
try {
api = await vegaEmbed(el, spec, embedOptions);
} catch (error) {
showError(error)
return;
}
finalize = api.finalize;
// Debounce config
const wait = model.get("debounce_wait") ?? 10;
const debounceOpts = {leading: false, trailing: true};
if (model.get("max_wait") ?? true) {
debounceOpts["maxWait"] = wait;
}
const initialSelections = {};
for (const selectionName of Object.keys(model.get("_vl_selections"))) {
const storeName = `${selectionName}_store`;
const selectionHandler = (_, value) => {
const newSelections = cleanJson(model.get("_vl_selections") ?? {});
const store = cleanJson(api.view.data(storeName) ?? []);
newSelections[selectionName] = {value, store};
model.set("_vl_selections", newSelections);
model.save_changes();
};
api.view.addSignalListener(selectionName, lodashDebounce(selectionHandler, wait, debounceOpts));
initialSelections[selectionName] = {
value: cleanJson(api.view.signal(selectionName) ?? {}),
store: cleanJson(api.view.data(storeName) ?? [])
}
}
model.set("_vl_selections", initialSelections);
const initialParams = {};
for (const paramName of Object.keys(model.get("_params"))) {
const paramHandler = (_, value) => {
const newParams = JSON.parse(JSON.stringify(model.get("_params"))) || {};
newParams[paramName] = value;
model.set("_params", newParams);
model.save_changes();
};
api.view.addSignalListener(paramName, lodashDebounce(paramHandler, wait, debounceOpts));
initialParams[paramName] = api.view.signal(paramName) ?? null
}
model.set("_params", initialParams);
model.save_changes();
// Param change callback
model.on('change:_params', async (new_params) => {
for (const [param, value] of Object.entries(new_params.changed._params)) {
api.view.signal(param, value);
}
await api.view.runAsync();
});
// Add signal/data listeners
for (const watch of model.get("_js_watch_plan") ?? []) {
if (watch.namespace === "data") {
const dataHandler = (_, value) => {
model.set("_js_to_py_updates", [{
namespace: "data",
name: watch.name,
scope: watch.scope,
value: cleanJson(value)
}]);
model.save_changes();
};
addDataListener(api.view, watch.name, watch.scope, lodashDebounce(dataHandler, wait, debounceOpts))
} else if (watch.namespace === "signal") {
const signalHandler = (_, value) => {
model.set("_js_to_py_updates", [{
namespace: "signal",
name: watch.name,
scope: watch.scope,
value: cleanJson(value)
}]);
model.save_changes();
};
addSignalListener(api.view, watch.name, watch.scope, lodashDebounce(signalHandler, wait, debounceOpts))
}
}
// Add signal/data updaters
model.on('change:_py_to_js_updates', async (updates) => {
for (const update of updates.changed._py_to_js_updates ?? []) {
if (update.namespace === "signal") {
setSignalValue(api.view, update.name, update.scope, update.value);
} else if (update.namespace === "data") {
setDataValue(api.view, update.name, update.scope, update.value);
}
}
await api.view.runAsync();
});
}
model.on('change:spec', reembed);
model.on('change:embed_options', reembed);
model.on('change:debounce_wait', reembed);
model.on('change:max_wait', reembed);
await reembed();
}
function cleanJson(data) {
return JSON.parse(JSON.stringify(data))
}
function getNestedRuntime(view, scope) {
var runtime = view._runtime;
for (const index of scope) {
runtime = runtime.subcontext[index];
}
return runtime
}
function lookupSignalOp(view, name, scope) {
let parent_runtime = getNestedRuntime(view, scope);
return parent_runtime.signals[name] ?? null;
}
function dataRef(view, name, scope) {
let parent_runtime = getNestedRuntime(view, scope);
return parent_runtime.data[name];
}
export function setSignalValue(view, name, scope, value) {
let signal_op = lookupSignalOp(view, name, scope);
view.update(signal_op, value);
}
export function setDataValue(view, name, scope, value) {
let dataset = dataRef(view, name, scope);
let changeset = view.changeset().remove(() => true).insert(value)
dataset.modified = true;
view.pulse(dataset.input, changeset);
}
export function addSignalListener(view, name, scope, handler) {
let signal_op = lookupSignalOp(view, name, scope);
return addOperatorListener(
view,
name,
signal_op,
handler,
);
}
export function addDataListener(view, name, scope, handler) {
let dataset = dataRef(view, name, scope).values;
return addOperatorListener(
view,
name,
dataset,
handler,
);
}
// Private helpers from Vega for dealing with nested signals/data
function findOperatorHandler(op, handler) {
const h = (op._targets || [])
.filter(op => op._update && op._update.handler === handler);
return h.length ? h[0] : null;
}
function addOperatorListener(view, name, op, handler) {
let h = findOperatorHandler(op, handler);
if (!h) {
h = trap(view, () => handler(name, op.value));
h.handler = handler;
view.on(op, null, h);
}
return view;
}
function trap(view, fn) {
return !fn ? null : function() {
try {
fn.apply(this, arguments);
} catch (error) {
view.error(error);
}
};
}
export default { render }

View File

@@ -0,0 +1,404 @@
from __future__ import annotations
import json
import pathlib
from typing import Any
import anywidget
import traitlets
import altair as alt
from altair import TopLevelSpec
from altair.utils._vegafusion_data import (
compile_to_vegafusion_chart_state,
using_vegafusion,
)
from altair.utils.selection import IndexSelection, IntervalSelection, PointSelection
_here = pathlib.Path(__file__).parent
class Params(traitlets.HasTraits):
"""Traitlet class storing a JupyterChart's params."""
def __init__(self, trait_values):
super().__init__()
for key, value in trait_values.items():
if isinstance(value, (int, float)):
traitlet_type = traitlets.Float()
elif isinstance(value, str):
traitlet_type = traitlets.Unicode()
elif isinstance(value, list):
traitlet_type = traitlets.List()
elif isinstance(value, dict):
traitlet_type = traitlets.Dict()
else:
traitlet_type = traitlets.Any()
# Add the new trait.
self.add_traits(**{key: traitlet_type})
# Set the trait's value.
setattr(self, key, value)
def __repr__(self):
return f"Params({self.trait_values()})"
class Selections(traitlets.HasTraits):
"""Traitlet class storing a JupyterChart's selections."""
def __init__(self, trait_values):
super().__init__()
for key, value in trait_values.items():
if isinstance(value, IndexSelection):
traitlet_type = traitlets.Instance(IndexSelection)
elif isinstance(value, PointSelection):
traitlet_type = traitlets.Instance(PointSelection)
elif isinstance(value, IntervalSelection):
traitlet_type = traitlets.Instance(IntervalSelection)
else:
msg = f"Unexpected selection type: {type(value)}"
raise ValueError(msg)
# Add the new trait.
self.add_traits(**{key: traitlet_type})
# Set the trait's value.
setattr(self, key, value)
# Make read-only
self.observe(self._make_read_only, names=key)
def __repr__(self):
return f"Selections({self.trait_values()})"
def _make_read_only(self, change):
"""Work around to make traits read-only, but still allow us to change them internally."""
if change["name"] in self.traits() and change["old"] != change["new"]:
self._set_value(change["name"], change["old"])
msg = (
"Selections may not be set from Python.\n"
f"Attempted to set select: {change['name']}"
)
raise ValueError(msg)
def _set_value(self, key, value):
self.unobserve(self._make_read_only, names=key)
setattr(self, key, value)
self.observe(self._make_read_only, names=key)
def load_js_src() -> str:
return (_here / "js" / "index.js").read_text()
class JupyterChart(anywidget.AnyWidget):
_esm = load_js_src()
_css = r"""
.vega-embed {
/* Make sure action menu isn't cut off */
overflow: visible;
}
"""
# Public traitlets
chart = traitlets.Instance(TopLevelSpec, allow_none=True)
spec = traitlets.Dict(allow_none=True).tag(sync=True)
debounce_wait = traitlets.Float(default_value=10).tag(sync=True)
max_wait = traitlets.Bool(default_value=True).tag(sync=True)
local_tz = traitlets.Unicode(default_value=None, allow_none=True).tag(sync=True)
debug = traitlets.Bool(default_value=False)
embed_options = traitlets.Dict(default_value=None, allow_none=True).tag(sync=True)
# Internal selection traitlets
_selection_types = traitlets.Dict()
_vl_selections = traitlets.Dict().tag(sync=True)
# Internal param traitlets
_params = traitlets.Dict().tag(sync=True)
# Internal comm traitlets for VegaFusion support
_chart_state = traitlets.Any(allow_none=True)
_js_watch_plan = traitlets.Any(allow_none=True).tag(sync=True)
_js_to_py_updates = traitlets.Any(allow_none=True).tag(sync=True)
_py_to_js_updates = traitlets.Any(allow_none=True).tag(sync=True)
# Track whether charts are configured for offline use
_is_offline = False
@classmethod
def enable_offline(cls, offline: bool = True):
"""
Configure JupyterChart's offline behavior.
Parameters
----------
offline: bool
If True, configure JupyterChart to operate in offline mode where JavaScript
dependencies are loaded from vl-convert.
If False, configure it to operate in online mode where JavaScript dependencies
are loaded from CDN dynamically. This is the default behavior.
"""
from altair.utils._importers import import_vl_convert, vl_version_for_vl_convert
if offline:
if cls._is_offline:
# Already offline
return
vlc = import_vl_convert()
src_lines = load_js_src().split("\n")
# Remove leading lines with only whitespace, comments, or imports
while src_lines and (
len(src_lines[0].strip()) == 0
or src_lines[0].startswith("import")
or src_lines[0].startswith("//")
):
src_lines.pop(0)
src = "\n".join(src_lines)
# vl-convert's javascript_bundle function creates a self-contained JavaScript bundle
# for JavaScript snippets that import from a small set of dependencies that
# vl-convert includes. To see the available imports and their imported names, run
# import vl_convert as vlc
# help(vlc.javascript_bundle)
bundled_src = vlc.javascript_bundle(
src, vl_version=vl_version_for_vl_convert()
)
cls._esm = bundled_src
cls._is_offline = True
else:
cls._esm = load_js_src()
cls._is_offline = False
def __init__(
self,
chart: TopLevelSpec,
debounce_wait: int = 10,
max_wait: bool = True,
debug: bool = False,
embed_options: dict | None = None,
**kwargs: Any,
):
"""
Jupyter Widget for displaying and updating Altair Charts, and retrieving selection and parameter values.
Parameters
----------
chart: Chart
Altair Chart instance
debounce_wait: int
Debouncing wait time in milliseconds. Updates will be sent from the client to the kernel
after debounce_wait milliseconds of no chart interactions.
max_wait: bool
If True (default), updates will be sent from the client to the kernel every debounce_wait
milliseconds even if there are ongoing chart interactions. If False, updates will not be
sent until chart interactions have completed.
debug: bool
If True, debug messages will be printed
embed_options: dict
Options to pass to vega-embed.
See https://github.com/vega/vega-embed?tab=readme-ov-file#options
"""
self.params = Params({})
self.selections = Selections({})
super().__init__(
chart=chart,
debounce_wait=debounce_wait,
max_wait=max_wait,
debug=debug,
embed_options=embed_options,
**kwargs,
)
@traitlets.observe("chart")
def _on_change_chart(self, change): # noqa: C901
"""Updates the JupyterChart's internal state when the wrapped Chart instance changes."""
new_chart = change.new
selection_watches = []
selection_types = {}
initial_params = {}
initial_vl_selections = {}
empty_selections = {}
if new_chart is None:
with self.hold_sync():
self.spec = None
self._selection_types = selection_types
self._vl_selections = initial_vl_selections
self._params = initial_params
return
params = getattr(new_chart, "params", [])
if params is not alt.Undefined:
for param in new_chart.params:
if isinstance(param.name, alt.ParameterName):
clean_name = param.name.to_json().strip('"')
else:
clean_name = param.name
select = getattr(param, "select", alt.Undefined)
if select != alt.Undefined:
if not isinstance(select, dict):
select = select.to_dict()
select_type = select["type"]
if select_type == "point":
if not (
select.get("fields", None) or select.get("encodings", None)
):
# Point selection with no associated fields or encodings specified.
# This is an index-based selection
selection_types[clean_name] = "index"
empty_selections[clean_name] = IndexSelection(
name=clean_name, value=[], store=[]
)
else:
selection_types[clean_name] = "point"
empty_selections[clean_name] = PointSelection(
name=clean_name, value=[], store=[]
)
elif select_type == "interval":
selection_types[clean_name] = "interval"
empty_selections[clean_name] = IntervalSelection(
name=clean_name, value={}, store=[]
)
else:
msg = f"Unexpected selection type {select.type}"
raise ValueError(msg)
selection_watches.append(clean_name)
initial_vl_selections[clean_name] = {"value": None, "store": []}
else:
clean_value = param.value if param.value != alt.Undefined else None
initial_params[clean_name] = clean_value
# Handle the params generated by transforms
for param_name in collect_transform_params(new_chart):
initial_params[param_name] = None
# Setup params
self.params = Params(initial_params)
def on_param_traitlet_changed(param_change):
new_params = dict(self._params)
new_params[param_change["name"]] = param_change["new"]
self._params = new_params
self.params.observe(on_param_traitlet_changed)
# Setup selections
self.selections = Selections(empty_selections)
# Update properties all together
with self.hold_sync():
if using_vegafusion():
if self.local_tz is None:
self.spec = None
def on_local_tz_change(change):
self._init_with_vegafusion(change["new"])
self.observe(on_local_tz_change, ["local_tz"])
else:
self._init_with_vegafusion(self.local_tz)
else:
self.spec = new_chart.to_dict()
self._selection_types = selection_types
self._vl_selections = initial_vl_selections
self._params = initial_params
def _init_with_vegafusion(self, local_tz: str):
if self.chart is not None:
vegalite_spec = self.chart.to_dict(context={"pre_transform": False})
with self.hold_sync():
self._chart_state = compile_to_vegafusion_chart_state(
vegalite_spec, local_tz
)
self._js_watch_plan = self._chart_state.get_watch_plan()[
"client_to_server"
]
self.spec = self._chart_state.get_transformed_spec()
# Callback to update chart state and send updates back to client
def on_js_to_py_updates(change):
if self.debug:
updates_str = json.dumps(change["new"], indent=2)
print(
f"JavaScript to Python VegaFusion updates:\n {updates_str}"
)
updates = self._chart_state.update(change["new"])
if self.debug:
updates_str = json.dumps(updates, indent=2)
print(
f"Python to JavaScript VegaFusion updates:\n {updates_str}"
)
self._py_to_js_updates = updates
self.observe(on_js_to_py_updates, ["_js_to_py_updates"])
@traitlets.observe("_params")
def _on_change_params(self, change):
for param_name, value in change.new.items():
setattr(self.params, param_name, value)
@traitlets.observe("_vl_selections")
def _on_change_selections(self, change):
"""Updates the JupyterChart's public selections traitlet in response to changes that the JavaScript logic makes to the internal _selections traitlet."""
for selection_name, selection_dict in change.new.items():
value = selection_dict["value"]
store = selection_dict["store"]
selection_type = self._selection_types[selection_name]
if selection_type == "index":
self.selections._set_value(
selection_name,
IndexSelection.from_vega(selection_name, signal=value, store=store),
)
elif selection_type == "point":
self.selections._set_value(
selection_name,
PointSelection.from_vega(selection_name, signal=value, store=store),
)
elif selection_type == "interval":
self.selections._set_value(
selection_name,
IntervalSelection.from_vega(
selection_name, signal=value, store=store
),
)
def collect_transform_params(chart: TopLevelSpec) -> set[str]:
"""
Collect the names of params that are defined by transforms.
Parameters
----------
chart: Chart from which to extract transform params
Returns
-------
set of param names
"""
transform_params = set()
# Handle recursive case
for prop in ("layer", "concat", "hconcat", "vconcat"):
for child in getattr(chart, prop, []):
transform_params.update(collect_transform_params(child))
# Handle chart's own transforms
transforms = getattr(chart, "transform", [])
transforms = transforms if transforms != alt.Undefined else []
for tx in transforms:
if hasattr(tx, "param"):
transform_params.add(tx.param)
return transform_params

View File

@@ -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)

View File

@@ -0,0 +1,96 @@
"""Public types to ease integrating with `altair`."""
from __future__ import annotations
__all__ = [
"ChannelAngle",
"ChannelColor",
"ChannelColumn",
"ChannelDescription",
"ChannelDetail",
"ChannelFacet",
"ChannelFill",
"ChannelFillOpacity",
"ChannelHref",
"ChannelKey",
"ChannelLatitude",
"ChannelLatitude2",
"ChannelLongitude",
"ChannelLongitude2",
"ChannelOpacity",
"ChannelOrder",
"ChannelRadius",
"ChannelRadius2",
"ChannelRow",
"ChannelShape",
"ChannelSize",
"ChannelStroke",
"ChannelStrokeDash",
"ChannelStrokeOpacity",
"ChannelStrokeWidth",
"ChannelText",
"ChannelTheta",
"ChannelTheta2",
"ChannelTooltip",
"ChannelUrl",
"ChannelX",
"ChannelX2",
"ChannelXError",
"ChannelXError2",
"ChannelXOffset",
"ChannelY",
"ChannelY2",
"ChannelYError",
"ChannelYError2",
"ChannelYOffset",
"ChartType",
"EncodeKwds",
"Optional",
"is_chart_type",
]
from altair.utils.schemapi import Optional
from altair.vegalite.v5.api import ChartType, is_chart_type
from altair.vegalite.v5.schema.channels import (
ChannelAngle,
ChannelColor,
ChannelColumn,
ChannelDescription,
ChannelDetail,
ChannelFacet,
ChannelFill,
ChannelFillOpacity,
ChannelHref,
ChannelKey,
ChannelLatitude,
ChannelLatitude2,
ChannelLongitude,
ChannelLongitude2,
ChannelOpacity,
ChannelOrder,
ChannelRadius,
ChannelRadius2,
ChannelRow,
ChannelShape,
ChannelSize,
ChannelStroke,
ChannelStrokeDash,
ChannelStrokeOpacity,
ChannelStrokeWidth,
ChannelText,
ChannelTheta,
ChannelTheta2,
ChannelTooltip,
ChannelUrl,
ChannelX,
ChannelX2,
ChannelXError,
ChannelXError2,
ChannelXOffset,
ChannelY,
ChannelY2,
ChannelYError,
ChannelYError2,
ChannelYOffset,
EncodeKwds,
)

View File

@@ -0,0 +1,37 @@
from .core import (
SHORTHAND_KEYS,
display_traceback,
infer_encoding_types,
infer_vegalite_type_for_pandas,
parse_shorthand,
sanitize_narwhals_dataframe,
sanitize_pandas_dataframe,
update_nested,
use_signature,
)
from .deprecation import AltairDeprecationWarning, deprecated, deprecated_warn
from .html import spec_to_html
from .plugin_registry import PluginRegistry
from .schemapi import Optional, SchemaBase, SchemaLike, Undefined, is_undefined
__all__ = (
"SHORTHAND_KEYS",
"AltairDeprecationWarning",
"Optional",
"PluginRegistry",
"SchemaBase",
"SchemaLike",
"Undefined",
"deprecated",
"deprecated_warn",
"display_traceback",
"infer_encoding_types",
"infer_vegalite_type_for_pandas",
"is_undefined",
"parse_shorthand",
"sanitize_narwhals_dataframe",
"sanitize_pandas_dataframe",
"spec_to_html",
"update_nested",
"use_signature",
)

View File

@@ -0,0 +1,164 @@
# DataFrame Interchange Protocol Types
# Copied from https://data-apis.org/dataframe-protocol/latest/API.html,
# changed ABCs to Protocols, and subset the type hints to only those that are
# relevant for Altair.
#
# These classes are only for use in type signatures
from __future__ import annotations
import enum
from typing import TYPE_CHECKING, Any, Protocol
if TYPE_CHECKING:
from collections.abc import Iterable
class DtypeKind(enum.IntEnum):
"""
Integer enum for data types.
Attributes
----------
INT : int
Matches to signed integer data type.
UINT : int
Matches to unsigned integer data type.
FLOAT : int
Matches to floating point data type.
BOOL : int
Matches to boolean data type.
STRING : int
Matches to string data type (UTF-8 encoded).
DATETIME : int
Matches to datetime data type.
CATEGORICAL : int
Matches to categorical data type.
"""
INT = 0
UINT = 1
FLOAT = 2
BOOL = 20
STRING = 21 # UTF-8
DATETIME = 22
CATEGORICAL = 23
# Type hint of first element would actually be DtypeKind but can't use that
# as other libraries won't use an instance of our own Enum in this module but have
# their own. Type checkers will raise an error on that even though the enums
# are identical.
class Column(Protocol):
@property
def dtype(self) -> tuple[Any, int, str, str]:
"""
Dtype description as a tuple ``(kind, bit-width, format string, endianness)``.
Bit-width : the number of bits as an integer
Format string : data type description format string in Apache Arrow C
Data Interface format.
Endianness : current only native endianness (``=``) is supported
Notes
-----
- Kind specifiers are aligned with DLPack where possible (hence the
jump to 20, leave enough room for future extension)
- Masks must be specified as boolean with either bit width 1 (for bit
masks) or 8 (for byte masks).
- Dtype width in bits was preferred over bytes
- Endianness isn't too useful, but included now in case in the future
we need to support non-native endianness
- Went with Apache Arrow format strings over NumPy format strings
because they're more complete from a dataframe perspective
- Format strings are mostly useful for datetime specification, and
for categoricals.
- For categoricals, the format string describes the type of the
categorical in the data buffer. In case of a separate encoding of
the categorical (e.g. an integer to string mapping), this can
be derived from ``self.describe_categorical``.
- Data types not included: complex, Arrow-style null, binary, decimal,
and nested (list, struct, map, union) dtypes.
"""
...
# Have to use a generic Any return type as not all libraries who implement
# the dataframe interchange protocol implement the TypedDict that is usually
# returned here in the same way. As TypedDicts are invariant, even a slight change
# will lead to an error by a type checker. See PR in which this code was added
# for details.
@property
def describe_categorical(self) -> Any:
"""
If the dtype is categorical, there are two options.
- There are only values in the data buffer.
- There is a separate non-categorical Column encoding categorical values.
Raises TypeError if the dtype is not categorical
Returns the dictionary with description on how to interpret the data buffer:
- "is_ordered" : bool, whether the ordering of dictionary indices is
semantically meaningful.
- "is_dictionary" : bool, whether a mapping of
categorical values to other objects exists
- "categories" : Column representing the (implicit) mapping of indices to
category values (e.g. an array of cat1, cat2, ...).
None if not a dictionary-style categorical.
TBD: are there any other in-memory representations that are needed?
"""
...
class DataFrame(Protocol):
"""
A data frame class, with only the methods required by the interchange protocol defined.
A "data frame" represents an ordered collection of named columns.
A column's "name" must be a unique string.
Columns may be accessed by name or by position.
This could be a public data frame class, or an object with the methods and
attributes defined on this DataFrame class could be returned from the
``__dataframe__`` method of a public data frame class in a library adhering
to the dataframe interchange protocol specification.
"""
def __dataframe__(
self, nan_as_null: bool = False, allow_copy: bool = True
) -> DataFrame:
"""
Construct a new exchange object, potentially changing the parameters.
``nan_as_null`` is a keyword intended for the consumer to tell the
producer to overwrite null values in the data with ``NaN``.
It is intended for cases where the consumer does not support the bit
mask or byte mask that is the producer's native representation.
``allow_copy`` is a keyword that defines whether or not the library is
allowed to make a copy of the data. For example, copying data would be
necessary if a library supports strided buffers, given that this protocol
specifies contiguous buffers.
"""
...
def column_names(self) -> Iterable[str]:
"""Return an iterator yielding the column names."""
...
def get_column_by_name(self, name: str) -> Column:
"""Return the column whose name is the indicated name."""
...
def get_chunks(self, n_chunks: int | None = None) -> Iterable[DataFrame]:
"""
Return an iterator yielding the chunks.
By default (None), yields the chunks that the data is stored as by the
producer. If given, ``n_chunks`` must be a multiple of
``self.num_chunks()``, meaning the producer must subdivide each chunk
before yielding it.
Note that the producer must ensure that all columns are chunked the
same way.
"""
...

View File

@@ -0,0 +1,113 @@
from __future__ import annotations
from importlib.metadata import version as importlib_version
from typing import TYPE_CHECKING
from packaging.version import Version
if TYPE_CHECKING:
from types import ModuleType
def import_vegafusion() -> ModuleType:
min_version = "1.5.0"
try:
import vegafusion as vf
version = importlib_version("vegafusion")
if Version(version) >= Version("2.0.0a0"):
# In VegaFusion 2.0 there is no vegafusion-python-embed package
return vf
else:
embed_version = importlib_version("vegafusion-python-embed")
if version != embed_version or Version(version) < Version(min_version):
msg = (
"The versions of the vegafusion and vegafusion-python-embed packages must match\n"
f"and must be version {min_version} or greater.\n"
f"Found:\n"
f" - vegafusion=={version}\n"
f" - vegafusion-python-embed=={embed_version}\n"
)
raise RuntimeError(msg)
return vf
except ImportError as err:
msg = (
'The "vegafusion" data transformer and chart.transformed_data feature requires\n'
f"version {min_version} or greater of the 'vegafusion-python-embed' and 'vegafusion' packages.\n"
"These can be installed with pip using:\n"
f' pip install "vegafusion[embed]>={min_version}"\n'
"Or with conda using:\n"
f' conda install -c conda-forge "vegafusion-python-embed>={min_version}" '
f'"vegafusion>={min_version}"\n\n'
f"ImportError: {err.args[0]}"
)
raise ImportError(msg) from err
def import_vl_convert() -> ModuleType:
min_version = "1.6.0"
try:
version = importlib_version("vl-convert-python")
if Version(version) < Version(min_version):
msg = (
f"The vl-convert-python package must be version {min_version} or greater. "
f"Found version {version}"
)
raise RuntimeError(msg)
import vl_convert as vlc
return vlc
except ImportError as err:
msg = (
f"The vl-convert Vega-Lite compiler and file export feature requires\n"
f"version {min_version} or greater of the 'vl-convert-python' package. \n"
f"This can be installed with pip using:\n"
f' pip install "vl-convert-python>={min_version}"\n'
"or conda:\n"
f' conda install -c conda-forge "vl-convert-python>={min_version}"\n\n'
f"ImportError: {err.args[0]}"
)
raise ImportError(msg) from err
def vl_version_for_vl_convert() -> str:
from altair.vegalite import SCHEMA_VERSION
# Compute VlConvert's vl_version string (of the form 'v5_2')
# from SCHEMA_VERSION (of the form 'v5.2.0')
return "_".join(SCHEMA_VERSION.split(".")[:2])
def import_pyarrow_interchange() -> ModuleType:
min_version = "11.0.0"
try:
version = importlib_version("pyarrow")
if Version(version) < Version(min_version):
msg = (
f"The pyarrow package must be version {min_version} or greater. "
f"Found version {version}"
)
raise RuntimeError(msg)
import pyarrow.interchange as pi
return pi
except ImportError as err:
msg = (
f"Usage of the DataFrame Interchange Protocol requires\n"
f"version {min_version} or greater of the pyarrow package. \n"
f"This can be installed with pip using:\n"
f' pip install "pyarrow>={min_version}"\n'
"or conda:\n"
f' conda install -c conda-forge "pyarrow>={min_version}"\n\n'
f"ImportError: {err.args[0]}"
)
raise ImportError(msg) from err
def pyarrow_available() -> bool:
try:
import_pyarrow_interchange()
return True
except (ImportError, RuntimeError):
return False

View File

@@ -0,0 +1,75 @@
from __future__ import annotations
import webbrowser
from http.server import BaseHTTPRequestHandler, HTTPServer
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Iterable
def open_html_in_browser(
html: str | bytes,
using: str | Iterable[str] | None = None,
port: int | None = None,
) -> None:
"""
Display an html document in a web browser without creating a temp file.
Instantiates a simple http server and uses the webbrowser module to
open the server's URL
Parameters
----------
html: str
HTML string to display
using: str or iterable of str
Name of the web browser to open (e.g. "chrome", "firefox", etc.).
If an iterable, choose the first browser available on the system.
If none, choose the system default browser.
port: int
Port to use. Defaults to a random port
"""
# Encode html to bytes
html_bytes = html.encode("utf8") if isinstance(html, str) else html
browser = None
if using is None:
browser = webbrowser.get(None)
else:
# normalize using to an iterable
if isinstance(using, str):
using = [using]
for browser_key in using:
try:
browser = webbrowser.get(browser_key)
if browser is not None:
break
except webbrowser.Error:
pass
if browser is None:
raise ValueError("Failed to locate a browser with name in " + str(using))
class OneShotRequestHandler(BaseHTTPRequestHandler):
def do_GET(self) -> None:
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
bufferSize = 1024 * 1024
for i in range(0, len(html_bytes), bufferSize):
self.wfile.write(html_bytes[i : i + bufferSize])
def log_message(self, format, *args):
# Silence stderr logging
pass
# Use specified port if provided, otherwise choose a random port (port value of 0)
server = HTTPServer(
("127.0.0.1", port if port is not None else 0), OneShotRequestHandler
)
browser.open(f"http://127.0.0.1:{server.server_port}")
server.handle_request()

View File

@@ -0,0 +1,567 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any, overload
from altair import (
Chart,
ConcatChart,
ConcatSpecGenericSpec,
FacetChart,
FacetedUnitSpec,
FacetSpec,
HConcatChart,
HConcatSpecGenericSpec,
LayerChart,
LayerSpec,
NonNormalizedSpec,
TopLevelConcatSpec,
TopLevelFacetSpec,
TopLevelHConcatSpec,
TopLevelLayerSpec,
TopLevelUnitSpec,
TopLevelVConcatSpec,
UnitSpec,
UnitSpecWithFrame,
VConcatChart,
VConcatSpecGenericSpec,
data_transformers,
)
from altair.utils._vegafusion_data import get_inline_tables, import_vegafusion
from altair.utils.schemapi import Undefined
if TYPE_CHECKING:
import sys
from collections.abc import Iterable
if sys.version_info >= (3, 10):
from typing import TypeAlias
else:
from typing_extensions import TypeAlias
from altair.typing import ChartType
from altair.utils.core import DataFrameLike
Scope: TypeAlias = tuple[int, ...]
FacetMapping: TypeAlias = dict[tuple[str, Scope], tuple[str, Scope]]
# For the transformed_data functionality, the chart classes in the values
# can be considered equivalent to the chart class in the key.
_chart_class_mapping = {
Chart: (
Chart,
TopLevelUnitSpec,
FacetedUnitSpec,
UnitSpec,
UnitSpecWithFrame,
NonNormalizedSpec,
),
LayerChart: (LayerChart, TopLevelLayerSpec, LayerSpec),
ConcatChart: (ConcatChart, TopLevelConcatSpec, ConcatSpecGenericSpec),
HConcatChart: (HConcatChart, TopLevelHConcatSpec, HConcatSpecGenericSpec),
VConcatChart: (VConcatChart, TopLevelVConcatSpec, VConcatSpecGenericSpec),
FacetChart: (FacetChart, TopLevelFacetSpec, FacetSpec),
}
@overload
def transformed_data(
chart: Chart | FacetChart,
row_limit: int | None = None,
exclude: Iterable[str] | None = None,
) -> DataFrameLike | None: ...
@overload
def transformed_data(
chart: LayerChart | HConcatChart | VConcatChart | ConcatChart,
row_limit: int | None = None,
exclude: Iterable[str] | None = None,
) -> list[DataFrameLike]: ...
def transformed_data(chart, row_limit=None, exclude=None):
"""
Evaluate a Chart's transforms.
Evaluate the data transforms associated with a Chart and return the
transformed data as one or more DataFrames
Parameters
----------
chart : Chart, FacetChart, LayerChart, HConcatChart, VConcatChart, or ConcatChart
Altair chart to evaluate transforms on
row_limit : int (optional)
Maximum number of rows to return for each DataFrame. None (default) for unlimited
exclude : iterable of str
Set of the names of charts to exclude
Returns
-------
DataFrame or list of DataFrames or None
If input chart is a Chart or Facet Chart, returns a DataFrame of the
transformed data. Otherwise, returns a list of DataFrames of the
transformed data
"""
vf = import_vegafusion()
# Add mark if none is specified to satisfy Vega-Lite
if isinstance(chart, Chart) and chart.mark == Undefined:
chart = chart.mark_point()
# Deep copy chart so that we can rename marks without affecting caller
chart = chart.copy(deep=True)
# Ensure that all views are named so that we can look them up in the
# resulting Vega specification
chart_names = name_views(chart, 0, exclude=exclude)
# Compile to Vega and extract inline DataFrames
with data_transformers.enable("vegafusion"):
vega_spec = chart.to_dict(format="vega", context={"pre_transform": False})
inline_datasets = get_inline_tables(vega_spec)
# Build mapping from mark names to vega datasets
facet_mapping = get_facet_mapping(vega_spec)
dataset_mapping = get_datasets_for_view_names(vega_spec, chart_names, facet_mapping)
# Build a list of vega dataset names that corresponds to the order
# of the chart components
dataset_names = []
for chart_name in chart_names:
if chart_name in dataset_mapping:
dataset_names.append(dataset_mapping[chart_name])
else:
msg = "Failed to locate all datasets"
raise ValueError(msg)
# Extract transformed datasets with VegaFusion
datasets, _ = vf.runtime.pre_transform_datasets(
vega_spec,
dataset_names,
row_limit=row_limit,
inline_datasets=inline_datasets,
)
if isinstance(chart, (Chart, FacetChart)):
# Return DataFrame (or None if it was excluded) if input was a simple Chart
if not datasets:
return None
else:
return datasets[0]
else:
# Otherwise return the list of DataFrames
return datasets
# The equivalent classes from _chart_class_mapping should also be added
# to the type hints below for `chart` as the function would also work for them.
# However, this was not possible so far as mypy then complains about
# "Overloaded function signatures 1 and 2 overlap with incompatible return types [misc]"
# This might be due to the complex type hierarchy of the chart classes.
# See also https://github.com/python/mypy/issues/5119
# and https://github.com/python/mypy/issues/4020 which show that mypy might not have
# a very consistent behavior for overloaded functions.
# The same error appeared when trying it with Protocols for the concat and layer charts.
# This function is only used internally and so we accept this inconsistency for now.
def name_views(
chart: ChartType, i: int = 0, exclude: Iterable[str] | None = None
) -> list[str]:
"""
Name unnamed chart views.
Name unnamed charts views so that we can look them up later in
the compiled Vega spec.
Note: This function mutates the input chart by applying names to
unnamed views.
Parameters
----------
chart : Chart, FacetChart, LayerChart, HConcatChart, VConcatChart, or ConcatChart
Altair chart to apply names to
i : int (default 0)
Starting chart index
exclude : iterable of str
Names of charts to exclude
Returns
-------
list of str
List of the names of the charts and subcharts
"""
exclude = set(exclude) if exclude is not None else set()
if isinstance(
chart, (_chart_class_mapping[Chart], _chart_class_mapping[FacetChart])
):
if chart.name not in exclude:
if chart.name in {None, Undefined}:
# Add name since none is specified
chart.name = Chart._get_name()
return [chart.name]
else:
return []
else:
subcharts: list[Any]
if isinstance(chart, _chart_class_mapping[LayerChart]):
subcharts = chart.layer
elif isinstance(chart, _chart_class_mapping[HConcatChart]):
subcharts = chart.hconcat
elif isinstance(chart, _chart_class_mapping[VConcatChart]):
subcharts = chart.vconcat
elif isinstance(chart, _chart_class_mapping[ConcatChart]):
subcharts = chart.concat
else:
msg = (
"transformed_data accepts an instance of "
"Chart, FacetChart, LayerChart, HConcatChart, VConcatChart, or ConcatChart\n"
f"Received value of type: {type(chart)}"
)
raise ValueError(msg)
chart_names: list[str] = []
for subchart in subcharts:
for name in name_views(subchart, i=i + len(chart_names), exclude=exclude):
chart_names.append(name)
return chart_names
def get_group_mark_for_scope(
vega_spec: dict[str, Any], scope: Scope
) -> dict[str, Any] | None:
"""
Get the group mark at a particular scope.
Parameters
----------
vega_spec : dict
Top-level Vega specification dictionary
scope : tuple of int
Scope tuple. If empty, the original Vega specification is returned.
Otherwise, the nested group mark at the scope specified is returned.
Returns
-------
dict or None
Top-level Vega spec (if scope is empty)
or group mark (if scope is non-empty)
or None (if group mark at scope does not exist)
Examples
--------
>>> spec = {
... "marks": [
... {"type": "group", "marks": [{"type": "symbol"}]},
... {"type": "group", "marks": [{"type": "rect"}]},
... ]
... }
>>> get_group_mark_for_scope(spec, (1,))
{'type': 'group', 'marks': [{'type': 'rect'}]}
"""
group = vega_spec
# Find group at scope
for scope_value in scope:
group_index = 0
child_group = None
for mark in group.get("marks", []):
if mark.get("type") == "group":
if group_index == scope_value:
child_group = mark
break
group_index += 1
if child_group is None:
return None
group = child_group
return group
def get_datasets_for_scope(vega_spec: dict[str, Any], scope: Scope) -> list[str]:
"""
Get the names of the datasets that are defined at a given scope.
Parameters
----------
vega_spec : dict
Top-leve Vega specification
scope : tuple of int
Scope tuple. If empty, the names of top-level datasets are returned
Otherwise, the names of the datasets defined in the nested group mark
at the specified scope are returned.
Returns
-------
list of str
List of the names of the datasets defined at the specified scope
Examples
--------
>>> spec = {
... "data": [{"name": "data1"}],
... "marks": [
... {
... "type": "group",
... "data": [{"name": "data2"}],
... "marks": [{"type": "symbol"}],
... },
... {
... "type": "group",
... "data": [
... {"name": "data3"},
... {"name": "data4"},
... ],
... "marks": [{"type": "rect"}],
... },
... ],
... }
>>> get_datasets_for_scope(spec, ())
['data1']
>>> get_datasets_for_scope(spec, (0,))
['data2']
>>> get_datasets_for_scope(spec, (1,))
['data3', 'data4']
Returns empty when no group mark exists at scope
>>> get_datasets_for_scope(spec, (1, 3))
[]
"""
group = get_group_mark_for_scope(vega_spec, scope) or {}
# get datasets from group
datasets = []
for dataset in group.get("data", []):
datasets.append(dataset["name"])
# Add facet dataset
facet_dataset = group.get("from", {}).get("facet", {}).get("name", None)
if facet_dataset:
datasets.append(facet_dataset)
return datasets
def get_definition_scope_for_data_reference(
vega_spec: dict[str, Any], data_name: str, usage_scope: Scope
) -> Scope | None:
"""
Return the scope that a dataset is defined at, for a given usage scope.
Parameters
----------
vega_spec: dict
Top-level Vega specification
data_name: str
The name of a dataset reference
usage_scope: tuple of int
The scope that the dataset is referenced in
Returns
-------
tuple of int
The scope where the referenced dataset is defined,
or None if no such dataset is found
Examples
--------
>>> spec = {
... "data": [{"name": "data1"}],
... "marks": [
... {
... "type": "group",
... "data": [{"name": "data2"}],
... "marks": [
... {
... "type": "symbol",
... "encode": {
... "update": {
... "x": {"field": "x", "data": "data1"},
... "y": {"field": "y", "data": "data2"},
... }
... },
... }
... ],
... }
... ],
... }
data1 is referenced at scope [0] and defined at scope []
>>> get_definition_scope_for_data_reference(spec, "data1", (0,))
()
data2 is referenced at scope [0] and defined at scope [0]
>>> get_definition_scope_for_data_reference(spec, "data2", (0,))
(0,)
If data2 is not visible at scope [] (the top level),
because it's defined in scope [0]
>>> repr(get_definition_scope_for_data_reference(spec, "data2", ()))
'None'
"""
for i in reversed(range(len(usage_scope) + 1)):
scope = usage_scope[:i]
datasets = get_datasets_for_scope(vega_spec, scope)
if data_name in datasets:
return scope
return None
def get_facet_mapping(group: dict[str, Any], scope: Scope = ()) -> FacetMapping:
"""
Create mapping from facet definitions to source datasets.
Parameters
----------
group : dict
Top-level Vega spec or nested group mark
scope : tuple of int
Scope of the group dictionary within a top-level Vega spec
Returns
-------
dict
Dictionary from (facet_name, facet_scope) to (dataset_name, dataset_scope)
Examples
--------
>>> spec = {
... "data": [{"name": "data1"}],
... "marks": [
... {
... "type": "group",
... "from": {
... "facet": {
... "name": "facet1",
... "data": "data1",
... "groupby": ["colA"],
... }
... },
... }
... ],
... }
>>> get_facet_mapping(spec)
{('facet1', (0,)): ('data1', ())}
"""
facet_mapping = {}
group_index = 0
mark_group = get_group_mark_for_scope(group, scope) or {}
for mark in mark_group.get("marks", []):
if mark.get("type", None) == "group":
# Get facet for this group
group_scope = (*scope, group_index)
facet = mark.get("from", {}).get("facet", None)
if facet is not None:
facet_name = facet.get("name", None)
facet_data = facet.get("data", None)
if facet_name is not None and facet_data is not None:
definition_scope = get_definition_scope_for_data_reference(
group, facet_data, scope
)
if definition_scope is not None:
facet_mapping[facet_name, group_scope] = (
facet_data,
definition_scope,
)
# Handle children recursively
child_mapping = get_facet_mapping(group, scope=group_scope)
facet_mapping.update(child_mapping)
group_index += 1
return facet_mapping
def get_from_facet_mapping(
scoped_dataset: tuple[str, Scope], facet_mapping: FacetMapping
) -> tuple[str, Scope]:
"""
Apply facet mapping to a scoped dataset.
Parameters
----------
scoped_dataset : (str, tuple of int)
A dataset name and scope tuple
facet_mapping : dict from (str, tuple of int) to (str, tuple of int)
The facet mapping produced by get_facet_mapping
Returns
-------
(str, tuple of int)
Dataset name and scope tuple that has been mapped as many times as possible
Examples
--------
Facet mapping as produced by get_facet_mapping
>>> facet_mapping = {
... ("facet1", (0,)): ("data1", ()),
... ("facet2", (0, 1)): ("facet1", (0,)),
... }
>>> get_from_facet_mapping(("facet2", (0, 1)), facet_mapping)
('data1', ())
"""
while scoped_dataset in facet_mapping:
scoped_dataset = facet_mapping[scoped_dataset]
return scoped_dataset
def get_datasets_for_view_names(
group: dict[str, Any],
vl_chart_names: list[str],
facet_mapping: FacetMapping,
scope: Scope = (),
) -> dict[str, tuple[str, Scope]]:
"""
Get the Vega datasets that correspond to the provided Altair view names.
Parameters
----------
group : dict
Top-level Vega spec or nested group mark
vl_chart_names : list of str
List of the Vega-Lite
facet_mapping : dict from (str, tuple of int) to (str, tuple of int)
The facet mapping produced by get_facet_mapping
scope : tuple of int
Scope of the group dictionary within a top-level Vega spec
Returns
-------
dict from str to (str, tuple of int)
Dict from Altair view names to scoped datasets
"""
datasets = {}
group_index = 0
mark_group = get_group_mark_for_scope(group, scope) or {}
for mark in mark_group.get("marks", []):
for vl_chart_name in vl_chart_names:
if mark.get("name", "") == f"{vl_chart_name}_cell":
data_name = mark.get("from", {}).get("facet", None).get("data", None)
scoped_data_name = (data_name, scope)
datasets[vl_chart_name] = get_from_facet_mapping(
scoped_data_name, facet_mapping
)
break
name = mark.get("name", "")
if mark.get("type", "") == "group":
group_data_names = get_datasets_for_view_names(
group, vl_chart_names, facet_mapping, scope=(*scope, group_index)
)
for k, v in group_data_names.items():
datasets.setdefault(k, v)
group_index += 1
else:
for vl_chart_name in vl_chart_names:
if name.startswith(vl_chart_name) and name.endswith("_marks"):
data_name = mark.get("from", {}).get("data", None)
scoped_data = get_definition_scope_for_data_reference(
group, data_name, scope
)
if scoped_data is not None:
datasets[vl_chart_name] = get_from_facet_mapping(
(data_name, scoped_data), facet_mapping
)
break
return datasets

View File

@@ -0,0 +1,304 @@
from __future__ import annotations
import uuid
from importlib.metadata import version as importlib_version
from typing import TYPE_CHECKING, Any, Callable, Final, TypedDict, Union, overload
from weakref import WeakValueDictionary
from narwhals.stable.v1.dependencies import is_into_dataframe
from packaging.version import Version
from altair.utils._importers import import_vegafusion
from altair.utils.core import DataFrameLike
from altair.utils.data import (
DataType,
MaxRowsError,
SupportsGeoInterface,
ToValuesReturnType,
)
from altair.vegalite.data import default_data_transformer
if TYPE_CHECKING:
import sys
from collections.abc import MutableMapping
from narwhals.stable.v1.typing import IntoDataFrame
from vegafusion.runtime import ChartState
if sys.version_info >= (3, 13):
from typing import TypeIs
else:
from typing_extensions import TypeIs
# Temporary storage for dataframes that have been extracted
# from charts by the vegafusion data transformer. Use a WeakValueDictionary
# rather than a dict so that the Python interpreter is free to garbage
# collect the stored DataFrames.
extracted_inline_tables: MutableMapping[str, DataFrameLike] = WeakValueDictionary()
# Special URL prefix that VegaFusion uses to denote that a
# dataset in a Vega spec corresponds to an entry in the `inline_datasets`
# kwarg of vf.runtime.pre_transform_spec().
VEGAFUSION_PREFIX: Final = "vegafusion+dataset://"
try:
VEGAFUSION_VERSION: Version | None = Version(importlib_version("vegafusion"))
except ImportError:
VEGAFUSION_VERSION = None
if VEGAFUSION_VERSION and Version("2.0.0a0") <= VEGAFUSION_VERSION:
def is_supported_by_vf(data: Any) -> TypeIs[DataFrameLike]:
# Test whether VegaFusion supports the data type
# VegaFusion v2 support narwhals-compatible DataFrames
return isinstance(data, DataFrameLike) or is_into_dataframe(data)
else:
def is_supported_by_vf(data: Any) -> TypeIs[DataFrameLike]:
return isinstance(data, DataFrameLike)
class _ToVegaFusionReturnUrlDict(TypedDict):
url: str
_VegaFusionReturnType = Union[_ToVegaFusionReturnUrlDict, ToValuesReturnType]
@overload
def vegafusion_data_transformer(
data: None = ..., max_rows: int = ...
) -> Callable[..., Any]: ...
@overload
def vegafusion_data_transformer(
data: DataFrameLike, max_rows: int = ...
) -> ToValuesReturnType: ...
@overload
def vegafusion_data_transformer(
data: dict | IntoDataFrame | SupportsGeoInterface, max_rows: int = ...
) -> _VegaFusionReturnType: ...
def vegafusion_data_transformer(
data: DataType | None = None, max_rows: int = 100000
) -> Callable[..., Any] | _VegaFusionReturnType:
"""VegaFusion Data Transformer."""
if data is None:
return vegafusion_data_transformer
if is_supported_by_vf(data) and not isinstance(data, SupportsGeoInterface):
table_name = f"table_{uuid.uuid4()}".replace("-", "_")
extracted_inline_tables[table_name] = data
return {"url": VEGAFUSION_PREFIX + table_name}
else:
# Use default transformer for geo interface objects
# # (e.g. a geopandas GeoDataFrame)
# Or if we don't recognize data type
return default_data_transformer(data)
def get_inline_table_names(vega_spec: dict[str, Any]) -> set[str]:
"""
Get a set of the inline datasets names in the provided Vega spec.
Inline datasets are encoded as URLs that start with the table://
prefix.
Parameters
----------
vega_spec: dict
A Vega specification dict
Returns
-------
set of str
Set of the names of the inline datasets that are referenced
in the specification.
Examples
--------
>>> spec = {
... "data": [
... {"name": "foo", "url": "https://path/to/file.csv"},
... {"name": "bar", "url": "vegafusion+dataset://inline_dataset_123"},
... ]
... }
>>> get_inline_table_names(spec)
{'inline_dataset_123'}
"""
table_names = set()
# Process datasets
for data in vega_spec.get("data", []):
url = data.get("url", "")
if url.startswith(VEGAFUSION_PREFIX):
name = url[len(VEGAFUSION_PREFIX) :]
table_names.add(name)
# Recursively process child marks, which may have their own datasets
for mark in vega_spec.get("marks", []):
table_names.update(get_inline_table_names(mark))
return table_names
def get_inline_tables(vega_spec: dict[str, Any]) -> dict[str, DataFrameLike]:
"""
Get the inline tables referenced by a Vega specification.
Note: This function should only be called on a Vega spec that corresponds
to a chart that was processed by the vegafusion_data_transformer.
Furthermore, this function may only be called once per spec because
the returned dataframes are deleted from internal storage.
Parameters
----------
vega_spec: dict
A Vega specification dict
Returns
-------
dict from str to dataframe
dict from inline dataset name to dataframe object
"""
inline_names = get_inline_table_names(vega_spec)
# exclude named dataset that was provided by the user,
# or dataframes that have been deleted.
table_names = inline_names.intersection(extracted_inline_tables)
return {k: extracted_inline_tables.pop(k) for k in table_names}
def compile_to_vegafusion_chart_state(
vegalite_spec: dict[str, Any], local_tz: str
) -> ChartState:
"""
Compile a Vega-Lite spec to a VegaFusion ChartState.
Note: This function should only be called on a Vega-Lite spec
that was generated with the "vegafusion" data transformer enabled.
In particular, this spec may contain references to extract datasets
using table:// prefixed URLs.
Parameters
----------
vegalite_spec: dict
A Vega-Lite spec that was generated from an Altair chart with
the "vegafusion" data transformer enabled
local_tz: str
Local timezone name (e.g. 'America/New_York')
Returns
-------
ChartState
A VegaFusion ChartState object
"""
# Local import to avoid circular ImportError
from altair import data_transformers, vegalite_compilers
vf = import_vegafusion()
# Compile Vega-Lite spec to Vega
compiler = vegalite_compilers.get()
if compiler is None:
msg = "No active vega-lite compiler plugin found"
raise ValueError(msg)
vega_spec = compiler(vegalite_spec)
# Retrieve dict of inline tables referenced by the spec
inline_tables = get_inline_tables(vega_spec)
# Pre-evaluate transforms in vega spec with vegafusion
row_limit = data_transformers.options.get("max_rows", None)
chart_state = vf.runtime.new_chart_state(
vega_spec,
local_tz=local_tz,
inline_datasets=inline_tables,
row_limit=row_limit,
)
# Check from row limit warning and convert to MaxRowsError
handle_row_limit_exceeded(row_limit, chart_state.get_warnings())
return chart_state
def compile_with_vegafusion(vegalite_spec: dict[str, Any]) -> dict[str, Any]:
"""
Compile a Vega-Lite spec to Vega and pre-transform with VegaFusion.
Note: This function should only be called on a Vega-Lite spec
that was generated with the "vegafusion" data transformer enabled.
In particular, this spec may contain references to extract datasets
using table:// prefixed URLs.
Parameters
----------
vegalite_spec: dict
A Vega-Lite spec that was generated from an Altair chart with
the "vegafusion" data transformer enabled
Returns
-------
dict
A Vega spec that has been pre-transformed by VegaFusion
"""
# Local import to avoid circular ImportError
from altair import data_transformers, vegalite_compilers
vf = import_vegafusion()
# Compile Vega-Lite spec to Vega
compiler = vegalite_compilers.get()
if compiler is None:
msg = "No active vega-lite compiler plugin found"
raise ValueError(msg)
vega_spec = compiler(vegalite_spec)
# Retrieve dict of inline tables referenced by the spec
inline_tables = get_inline_tables(vega_spec)
# Pre-evaluate transforms in vega spec with vegafusion
row_limit = data_transformers.options.get("max_rows", None)
transformed_vega_spec, warnings = vf.runtime.pre_transform_spec(
vega_spec,
vf.get_local_tz(),
inline_datasets=inline_tables,
row_limit=row_limit,
)
# Check from row limit warning and convert to MaxRowsError
handle_row_limit_exceeded(row_limit, warnings)
return transformed_vega_spec
def handle_row_limit_exceeded(row_limit: int, warnings: list):
for warning in warnings:
if warning.get("type") == "RowLimitExceeded":
msg = (
"The number of dataset rows after filtering and aggregation exceeds\n"
f"the current limit of {row_limit}. Try adding an aggregation to reduce\n"
"the size of the dataset that must be loaded into the browser. Or, disable\n"
"the limit by calling alt.data_transformers.disable_max_rows(). Note that\n"
"disabling this limit may cause the browser to freeze or crash."
)
raise MaxRowsError(msg)
def using_vegafusion() -> bool:
"""Check whether the vegafusion data transformer is enabled."""
# Local import to avoid circular ImportError
from altair import data_transformers
return data_transformers.active == "vegafusion"

View File

@@ -0,0 +1,12 @@
from typing import Any, Callable
from altair.utils import PluginRegistry
# ==============================================================================
# Vega-Lite to Vega compiler registry
# ==============================================================================
VegaLiteCompilerType = Callable[[dict[str, Any]], dict[str, Any]]
class VegaLiteCompilerRegistry(PluginRegistry[VegaLiteCompilerType, dict[str, Any]]):
pass

View File

@@ -0,0 +1,981 @@
"""Utility routines."""
from __future__ import annotations
import itertools
import json
import re
import sys
import traceback
import warnings
from collections.abc import Iterator, Mapping, MutableMapping
from copy import deepcopy
from itertools import groupby
from operator import itemgetter
from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar, cast, overload
import jsonschema
import narwhals.stable.v1 as nw
from narwhals.stable.v1.dependencies import is_pandas_dataframe, is_polars_dataframe
from narwhals.stable.v1.typing import IntoDataFrame
from altair.utils.schemapi import SchemaBase, SchemaLike, Undefined
if sys.version_info >= (3, 12):
from typing import Protocol, TypeAliasType, runtime_checkable
else:
from typing_extensions import Protocol, TypeAliasType, runtime_checkable
if sys.version_info >= (3, 10):
from typing import Concatenate, ParamSpec
else:
from typing_extensions import Concatenate, ParamSpec
if TYPE_CHECKING:
import typing as t
import pandas as pd
from narwhals.stable.v1.typing import IntoExpr
from altair.utils._dfi_types import DataFrame as DfiDataFrame
from altair.vegalite.v5.schema._typing import StandardType_T as InferredVegaLiteType
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
class DataFrameLike(Protocol):
def __dataframe__(
self, nan_as_null: bool = False, allow_copy: bool = True
) -> DfiDataFrame: ...
TYPECODE_MAP = {
"ordinal": "O",
"nominal": "N",
"quantitative": "Q",
"temporal": "T",
"geojson": "G",
}
INV_TYPECODE_MAP = {v: k for k, v in TYPECODE_MAP.items()}
# aggregates from vega-lite version 4.6.0
AGGREGATES = [
"argmax",
"argmin",
"average",
"count",
"distinct",
"max",
"mean",
"median",
"min",
"missing",
"product",
"q1",
"q3",
"ci0",
"ci1",
"stderr",
"stdev",
"stdevp",
"sum",
"valid",
"values",
"variance",
"variancep",
"exponential",
"exponentialb",
]
# window aggregates from vega-lite version 4.6.0
WINDOW_AGGREGATES = [
"row_number",
"rank",
"dense_rank",
"percent_rank",
"cume_dist",
"ntile",
"lag",
"lead",
"first_value",
"last_value",
"nth_value",
]
# timeUnits from vega-lite version 4.17.0
TIMEUNITS = [
"year",
"quarter",
"month",
"week",
"day",
"dayofyear",
"date",
"hours",
"minutes",
"seconds",
"milliseconds",
"yearquarter",
"yearquartermonth",
"yearmonth",
"yearmonthdate",
"yearmonthdatehours",
"yearmonthdatehoursminutes",
"yearmonthdatehoursminutesseconds",
"yearweek",
"yearweekday",
"yearweekdayhours",
"yearweekdayhoursminutes",
"yearweekdayhoursminutesseconds",
"yeardayofyear",
"quartermonth",
"monthdate",
"monthdatehours",
"monthdatehoursminutes",
"monthdatehoursminutesseconds",
"weekday",
"weeksdayhours",
"weekdayhours",
"weekdayhoursminutes",
"weekdayhoursminutesseconds",
"dayhours",
"dayhoursminutes",
"dayhoursminutesseconds",
"hoursminutes",
"hoursminutesseconds",
"minutesseconds",
"secondsmilliseconds",
"utcyear",
"utcquarter",
"utcmonth",
"utcweek",
"utcday",
"utcdayofyear",
"utcdate",
"utchours",
"utcminutes",
"utcseconds",
"utcmilliseconds",
"utcyearquarter",
"utcyearquartermonth",
"utcyearmonth",
"utcyearmonthdate",
"utcyearmonthdatehours",
"utcyearmonthdatehoursminutes",
"utcyearmonthdatehoursminutesseconds",
"utcyearweek",
"utcyearweekday",
"utcyearweekdayhours",
"utcyearweekdayhoursminutes",
"utcyearweekdayhoursminutesseconds",
"utcyeardayofyear",
"utcquartermonth",
"utcmonthdate",
"utcmonthdatehours",
"utcmonthdatehoursminutes",
"utcmonthdatehoursminutesseconds",
"utcweekday",
"utcweekdayhours",
"utcweekdayhoursminutes",
"utcweekdayhoursminutesseconds",
"utcdayhours",
"utcdayhoursminutes",
"utcdayhoursminutesseconds",
"utchoursminutes",
"utchoursminutesseconds",
"utcminutesseconds",
"utcsecondsmilliseconds",
]
VALID_TYPECODES = list(itertools.chain(iter(TYPECODE_MAP), iter(INV_TYPECODE_MAP)))
SHORTHAND_UNITS = {
"field": "(?P<field>.*)",
"type": "(?P<type>{})".format("|".join(VALID_TYPECODES)),
"agg_count": "(?P<aggregate>count)",
"op_count": "(?P<op>count)",
"aggregate": "(?P<aggregate>{})".format("|".join(AGGREGATES)),
"window_op": "(?P<op>{})".format("|".join(AGGREGATES + WINDOW_AGGREGATES)),
"timeUnit": "(?P<timeUnit>{})".format("|".join(TIMEUNITS)),
}
SHORTHAND_KEYS: frozenset[Literal["field", "aggregate", "type", "timeUnit"]] = (
frozenset(("field", "aggregate", "type", "timeUnit"))
)
def infer_vegalite_type_for_pandas(
data: Any,
) -> InferredVegaLiteType | tuple[InferredVegaLiteType, list[Any]]:
"""
From an array-like input, infer the correct vega typecode.
('ordinal', 'nominal', 'quantitative', or 'temporal').
Parameters
----------
data: Any
"""
# This is safe to import here, as this function is only called on pandas input.
from pandas.api.types import infer_dtype
typ = infer_dtype(data, skipna=False)
if typ in {
"floating",
"mixed-integer-float",
"integer",
"mixed-integer",
"complex",
}:
return "quantitative"
elif typ == "categorical" and hasattr(data, "cat") and data.cat.ordered:
return ("ordinal", data.cat.categories.tolist())
elif typ in {"string", "bytes", "categorical", "boolean", "mixed", "unicode"}:
return "nominal"
elif typ in {
"datetime",
"datetime64",
"timedelta",
"timedelta64",
"date",
"time",
"period",
}:
return "temporal"
else:
warnings.warn(
f"I don't know how to infer vegalite type from '{typ}'. "
"Defaulting to nominal.",
stacklevel=1,
)
return "nominal"
def merge_props_geom(feat: dict[str, Any]) -> dict[str, Any]:
"""
Merge properties with geometry.
* Overwrites 'type' and 'geometry' entries if existing.
"""
geom = {k: feat[k] for k in ("type", "geometry")}
try:
feat["properties"].update(geom)
props_geom = feat["properties"]
except (AttributeError, KeyError):
# AttributeError when 'properties' equals None
# KeyError when 'properties' is non-existing
props_geom = geom
return props_geom
def sanitize_geo_interface(geo: t.MutableMapping[Any, Any]) -> dict[str, Any]:
"""
Santize a geo_interface to prepare it for serialization.
* Make a copy
* Convert type array or _Array to list
* Convert tuples to lists (using json.loads/dumps)
* Merge properties with geometry
"""
geo = deepcopy(geo)
# convert type _Array or array to list
for key in geo:
if str(type(geo[key]).__name__).startswith(("_Array", "array")):
geo[key] = geo[key].tolist()
# convert (nested) tuples to lists
geo_dct: dict = json.loads(json.dumps(geo))
# sanitize features
if geo_dct["type"] == "FeatureCollection":
geo_dct = geo_dct["features"]
if len(geo_dct) > 0:
for idx, feat in enumerate(geo_dct):
geo_dct[idx] = merge_props_geom(feat)
elif geo_dct["type"] == "Feature":
geo_dct = merge_props_geom(geo_dct)
else:
geo_dct = {"type": "Feature", "geometry": geo_dct}
return geo_dct
def numpy_is_subtype(dtype: Any, subtype: Any) -> bool:
# This is only called on `numpy` inputs, so it's safe to import it here.
import numpy as np
try:
return np.issubdtype(dtype, subtype)
except (NotImplementedError, TypeError):
return False
def sanitize_pandas_dataframe(df: pd.DataFrame) -> pd.DataFrame: # noqa: C901
"""
Sanitize a DataFrame to prepare it for serialization.
* Make a copy
* Convert RangeIndex columns to strings
* Raise ValueError if column names are not strings
* Raise ValueError if it has a hierarchical index.
* Convert categoricals to strings.
* Convert np.bool_ dtypes to Python bool objects
* Convert np.int dtypes to Python int objects
* Convert floats to objects and replace NaNs/infs with None.
* Convert DateTime dtypes into appropriate string representations
* Convert Nullable integers to objects and replace NaN with None
* Convert Nullable boolean to objects and replace NaN with None
* convert dedicated string column to objects and replace NaN with None
* Raise a ValueError for TimeDelta dtypes
"""
# This is safe to import here, as this function is only called on pandas input.
# NumPy is a required dependency of pandas so is also safe to import.
import numpy as np
import pandas as pd
df = df.copy()
if isinstance(df.columns, pd.RangeIndex):
df.columns = df.columns.astype(str)
for col_name in df.columns:
if not isinstance(col_name, str):
msg = (
f"Dataframe contains invalid column name: {col_name!r}. "
"Column names must be strings"
)
raise ValueError(msg)
if isinstance(df.index, pd.MultiIndex):
msg = "Hierarchical indices not supported"
raise ValueError(msg)
if isinstance(df.columns, pd.MultiIndex):
msg = "Hierarchical indices not supported"
raise ValueError(msg)
def to_list_if_array(val):
if isinstance(val, np.ndarray):
return val.tolist()
else:
return val
for dtype_item in df.dtypes.items():
# We know that the column names are strings from the isinstance check
# further above but mypy thinks it is of type Hashable and therefore does not
# let us assign it to the col_name variable which is already of type str.
col_name = cast(str, dtype_item[0])
dtype = dtype_item[1]
dtype_name = str(dtype)
if dtype_name == "category":
# Work around bug in to_json for categorical types in older versions
# of pandas as they do not properly convert NaN values to null in to_json.
# We can probably remove this part once we require pandas >= 1.0
col = df[col_name].astype(object)
df[col_name] = col.where(col.notnull(), None)
elif dtype_name == "string":
# dedicated string datatype (since 1.0)
# https://pandas.pydata.org/pandas-docs/version/1.0.0/whatsnew/v1.0.0.html#dedicated-string-data-type
col = df[col_name].astype(object)
df[col_name] = col.where(col.notnull(), None)
elif dtype_name == "bool":
# convert numpy bools to objects; np.bool is not JSON serializable
df[col_name] = df[col_name].astype(object)
elif dtype_name == "boolean":
# dedicated boolean datatype (since 1.0)
# https://pandas.io/docs/user_guide/boolean.html
col = df[col_name].astype(object)
df[col_name] = col.where(col.notnull(), None)
elif dtype_name.startswith(("datetime", "timestamp")):
# Convert datetimes to strings. This needs to be a full ISO string
# with time, which is why we cannot use ``col.astype(str)``.
# This is because Javascript parses date-only times in UTC, but
# parses full ISO-8601 dates as local time, and dates in Vega and
# Vega-Lite are displayed in local time by default.
# (see https://github.com/vega/altair/issues/1027)
df[col_name] = (
df[col_name].apply(lambda x: x.isoformat()).replace("NaT", "")
)
elif dtype_name.startswith("timedelta"):
msg = (
f'Field "{col_name}" has type "{dtype}" which is '
"not supported by Altair. Please convert to "
"either a timestamp or a numerical value."
""
)
raise ValueError(msg)
elif dtype_name.startswith("geometry"):
# geopandas >=0.6.1 uses the dtype geometry. Continue here
# otherwise it will give an error on np.issubdtype(dtype, np.integer)
continue
elif (
dtype_name
in {
"Int8",
"Int16",
"Int32",
"Int64",
"UInt8",
"UInt16",
"UInt32",
"UInt64",
"Float32",
"Float64",
}
): # nullable integer datatypes (since 24.0) and nullable float datatypes (since 1.2.0)
# https://pandas.pydata.org/pandas-docs/version/0.25/whatsnew/v0.24.0.html#optional-integer-na-support
col = df[col_name].astype(object)
df[col_name] = col.where(col.notnull(), None)
elif numpy_is_subtype(dtype, np.integer):
# convert integers to objects; np.int is not JSON serializable
df[col_name] = df[col_name].astype(object)
elif numpy_is_subtype(dtype, np.floating):
# For floats, convert to Python float: np.float is not JSON serializable
# Also convert NaN/inf values to null, as they are not JSON serializable
col = df[col_name]
bad_values = col.isnull() | np.isinf(col)
df[col_name] = col.astype(object).where(~bad_values, None)
elif dtype == object: # noqa: E721
# Convert numpy arrays saved as objects to lists
# Arrays are not JSON serializable
col = df[col_name].astype(object).apply(to_list_if_array)
df[col_name] = col.where(col.notnull(), None)
return df
def sanitize_narwhals_dataframe(
data: nw.DataFrame[TIntoDataFrame],
) -> nw.DataFrame[TIntoDataFrame]:
"""Sanitize narwhals.DataFrame for JSON serialization."""
schema = data.schema
columns: list[IntoExpr] = []
# See https://github.com/vega/altair/issues/1027 for why this is necessary.
local_iso_fmt_string = "%Y-%m-%dT%H:%M:%S"
is_polars = is_polars_dataframe(data.to_native())
for name, dtype in schema.items():
if dtype == nw.Date and is_polars:
# Polars doesn't allow formatting `Date` with time directives.
# The date -> datetime cast is extremely fast compared with `to_string`
columns.append(
nw.col(name).cast(nw.Datetime).dt.to_string(local_iso_fmt_string)
)
elif dtype == nw.Date:
columns.append(nw.col(name).dt.to_string(local_iso_fmt_string))
elif dtype == nw.Datetime:
columns.append(nw.col(name).dt.to_string(f"{local_iso_fmt_string}%.f"))
elif dtype == nw.Duration:
msg = (
f'Field "{name}" has type "{dtype}" which is '
"not supported by Altair. Please convert to "
"either a timestamp or a numerical value."
""
)
raise ValueError(msg)
else:
columns.append(name)
return data.select(columns)
def to_eager_narwhals_dataframe(data: IntoDataFrame) -> nw.DataFrame[Any]:
"""
Wrap `data` in `narwhals.DataFrame`.
If `data` is not supported by Narwhals, but it is convertible
to a PyArrow table, then first convert to a PyArrow Table,
and then wrap in `narwhals.DataFrame`.
"""
data_nw = nw.from_native(data, eager_or_interchange_only=True)
if nw.get_level(data_nw) == "interchange":
# If Narwhals' support for `data`'s class is only metadata-level, then we
# use the interchange protocol to convert to a PyArrow Table.
from altair.utils.data import arrow_table_from_dfi_dataframe
pa_table = arrow_table_from_dfi_dataframe(data) # type: ignore[arg-type]
data_nw = nw.from_native(pa_table, eager_only=True)
return data_nw
def parse_shorthand( # noqa: C901
shorthand: dict[str, Any] | str,
data: IntoDataFrame | None = None,
parse_aggregates: bool = True,
parse_window_ops: bool = False,
parse_timeunits: bool = True,
parse_types: bool = True,
) -> dict[str, Any]:
"""
General tool to parse shorthand values.
These are of the form:
- "col_name"
- "col_name:O"
- "average(col_name)"
- "average(col_name):O"
Optionally, a dataframe may be supplied, from which the type
will be inferred if not specified in the shorthand.
Parameters
----------
shorthand : dict or string
The shorthand representation to be parsed
data : DataFrame, optional
If specified and of type DataFrame, then use these values to infer the
column type if not provided by the shorthand.
parse_aggregates : boolean
If True (default), then parse aggregate functions within the shorthand.
parse_window_ops : boolean
If True then parse window operations within the shorthand (default:False)
parse_timeunits : boolean
If True (default), then parse timeUnits from within the shorthand
parse_types : boolean
If True (default), then parse typecodes within the shorthand
Returns
-------
attrs : dict
a dictionary of attributes extracted from the shorthand
Examples
--------
>>> import pandas as pd
>>> data = pd.DataFrame({"foo": ["A", "B", "A", "B"], "bar": [1, 2, 3, 4]})
>>> parse_shorthand("name") == {"field": "name"}
True
>>> parse_shorthand("name:Q") == {"field": "name", "type": "quantitative"}
True
>>> parse_shorthand("average(col)") == {"aggregate": "average", "field": "col"}
True
>>> parse_shorthand("foo:O") == {"field": "foo", "type": "ordinal"}
True
>>> parse_shorthand("min(foo):Q") == {
... "aggregate": "min",
... "field": "foo",
... "type": "quantitative",
... }
True
>>> parse_shorthand("month(col)") == {
... "field": "col",
... "timeUnit": "month",
... "type": "temporal",
... }
True
>>> parse_shorthand("year(col):O") == {
... "field": "col",
... "timeUnit": "year",
... "type": "ordinal",
... }
True
>>> parse_shorthand("foo", data) == {"field": "foo", "type": "nominal"}
True
>>> parse_shorthand("bar", data) == {"field": "bar", "type": "quantitative"}
True
>>> parse_shorthand("bar:O", data) == {"field": "bar", "type": "ordinal"}
True
>>> parse_shorthand("sum(bar)", data) == {
... "aggregate": "sum",
... "field": "bar",
... "type": "quantitative",
... }
True
>>> parse_shorthand("count()", data) == {
... "aggregate": "count",
... "type": "quantitative",
... }
True
"""
from altair.utils.data import is_data_type
if not shorthand:
return {}
patterns = []
if parse_aggregates:
patterns.extend([r"{agg_count}\(\)"])
patterns.extend([r"{aggregate}\({field}\)"])
if parse_window_ops:
patterns.extend([r"{op_count}\(\)"])
patterns.extend([r"{window_op}\({field}\)"])
if parse_timeunits:
patterns.extend([r"{timeUnit}\({field}\)"])
patterns.extend([r"{field}"])
if parse_types:
patterns = list(itertools.chain(*((p + ":{type}", p) for p in patterns)))
regexps = (
re.compile(r"\A" + p.format(**SHORTHAND_UNITS) + r"\Z", re.DOTALL)
for p in patterns
)
# find matches depending on valid fields passed
if isinstance(shorthand, dict):
attrs = shorthand
else:
attrs = next(
exp.match(shorthand).groupdict() # type: ignore[union-attr]
for exp in regexps
if exp.match(shorthand) is not None
)
# Handle short form of the type expression
if "type" in attrs:
attrs["type"] = INV_TYPECODE_MAP.get(attrs["type"], attrs["type"])
# counts are quantitative by default
if attrs == {"aggregate": "count"}:
attrs["type"] = "quantitative"
# times are temporal by default
if "timeUnit" in attrs and "type" not in attrs:
attrs["type"] = "temporal"
# if data is specified and type is not, infer type from data
if "type" not in attrs and is_data_type(data):
unescaped_field = attrs["field"].replace("\\", "")
data_nw = nw.from_native(data, eager_or_interchange_only=True)
schema = data_nw.schema
if unescaped_field in schema:
column = data_nw[unescaped_field]
if schema[unescaped_field] in {
nw.Object,
nw.Unknown,
} and is_pandas_dataframe(data_nw.to_native()):
attrs["type"] = infer_vegalite_type_for_pandas(column.to_native())
else:
attrs["type"] = infer_vegalite_type_for_narwhals(column)
if isinstance(attrs["type"], tuple):
attrs["sort"] = attrs["type"][1]
attrs["type"] = attrs["type"][0]
# If an unescaped colon is still present, it's often due to an incorrect data type specification
# but could also be due to using a column name with ":" in it.
if (
"field" in attrs
and ":" in attrs["field"]
and attrs["field"][attrs["field"].rfind(":") - 1] != "\\"
):
raise ValueError(
'"{}" '.format(attrs["field"].split(":")[-1])
+ "is not one of the valid encoding data types: {}.".format(
", ".join(TYPECODE_MAP.values())
)
+ "\nFor more details, see https://altair-viz.github.io/user_guide/encodings/index.html#encoding-data-types. "
+ "If you are trying to use a column name that contains a colon, "
+ 'prefix it with a backslash; for example "column\\:name" instead of "column:name".'
)
return attrs
def infer_vegalite_type_for_narwhals(
column: nw.Series,
) -> InferredVegaLiteType | tuple[InferredVegaLiteType, list]:
dtype = column.dtype
if (
nw.is_ordered_categorical(column)
and not (categories := column.cat.get_categories()).is_empty()
):
return "ordinal", categories.to_list()
if dtype == nw.String or dtype == nw.Categorical or dtype == nw.Boolean: # noqa: PLR1714
return "nominal"
elif dtype.is_numeric():
return "quantitative"
elif dtype == nw.Datetime or dtype == nw.Date: # noqa: PLR1714
# We use `== nw.Datetime` to check for any kind of Datetime, regardless of time
# unit and time zone. Prefer this over `dtype in {nw.Datetime, nw.Date}`,
# see https://narwhals-dev.github.io/narwhals/backcompat.
return "temporal"
else:
msg = f"Unexpected DtypeKind: {dtype}"
raise ValueError(msg)
def use_signature(tp: Callable[P, Any], /):
"""
Use the signature and doc of ``tp`` for the decorated callable ``cb``.
- **Overload 1**: Decorating method
- **Overload 2**: Decorating function
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]: ... # pyright: ignore[reportOverlappingOverload]
@overload
def decorate(cb: WrapsFunc[R], /) -> WrappedFunc[P, R]: ... # pyright: ignore[reportOverlappingOverload]
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 {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]:
"""
Update nested dictionaries.
Parameters
----------
original : MutableMapping
the original (nested) dictionary, which will be updated in-place
update : Mapping
the nested dictionary of updates
copy : bool, default False
if True, then copy the original dictionary rather than modifying it
Returns
-------
original : MutableMapping
a reference to the (modified) original dict
Examples
--------
>>> original = {"x": {"b": 2, "c": 4}}
>>> update = {"x": {"b": 5, "d": 6}, "y": 40}
>>> update_nested(original, update) # doctest: +SKIP
{'x': {'b': 5, 'c': 4, 'd': 6}, 'y': 40}
>>> original # doctest: +SKIP
{'x': {'b': 5, 'c': 4, 'd': 6}, 'y': 40}
"""
if copy:
original = deepcopy(original)
for key, val in update.items():
if isinstance(val, Mapping):
orig_val = original.get(key, {})
if isinstance(orig_val, MutableMapping):
original[key] = update_nested(orig_val, val)
else:
original[key] = val
else:
original[key] = val
return original
def display_traceback(in_ipython: bool = True):
exc_info = sys.exc_info()
if in_ipython:
from IPython.core.getipython import get_ipython
ip = get_ipython()
else:
ip = None
if ip is not None:
ip.showtraceback(exc_info)
else:
traceback.print_exception(*exc_info)
_ChannelType = Literal["field", "datum", "value"]
_CHANNEL_CACHE: _ChannelCache
"""Singleton `_ChannelCache` instance.
Initialized on first use.
"""
class _ChannelCache:
channel_to_name: dict[type[SchemaBase], str]
name_to_channel: dict[str, dict[_ChannelType, type[SchemaBase]]]
@classmethod
def from_cache(cls) -> _ChannelCache:
global _CHANNEL_CACHE
try:
cached = _CHANNEL_CACHE
except NameError:
cached = cls.__new__(cls)
cached.channel_to_name = _init_channel_to_name() # pyright: ignore[reportAttributeAccessIssue]
cached.name_to_channel = _invert_group_channels(cached.channel_to_name)
_CHANNEL_CACHE = cached
return _CHANNEL_CACHE
def get_encoding(self, tp: type[Any], /) -> str:
if encoding := self.channel_to_name.get(tp):
return encoding
msg = f"positional of type {type(tp).__name__!r}"
raise NotImplementedError(msg)
def _wrap_in_channel(self, obj: Any, encoding: str, /):
if isinstance(obj, SchemaBase):
return obj
elif isinstance(obj, 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:
# Don't force validation here; some objects won't be valid until
# they're created in the context of a chart.
return tp.from_dict(obj, validate=False)
except jsonschema.ValidationError:
# our attempts at finding the correct class have failed
return obj
else:
warnings.warn(f"Unrecognized encoding channel {encoding!r}", stacklevel=1)
return obj
def infer_encoding_types(self, kwargs: dict[str, Any], /):
return {
encoding: self._wrap_in_channel(obj, encoding)
for encoding, obj in kwargs.items()
if obj is not Undefined
}
def _init_channel_to_name():
"""
Construct a dictionary of channel type to encoding name.
Note
----
The return type is not expressible using annotations, but is used
internally by `mypy`/`pyright` and avoids the need for type ignores.
Returns
-------
mapping: dict[type[`<subclass of FieldChannelMixin and SchemaBase>`] | type[`<subclass of ValueChannelMixin and SchemaBase>`] | type[`<subclass of DatumChannelMixin and SchemaBase>`], str]
"""
from altair.vegalite.v5.schema import channels as ch
mixins = ch.FieldChannelMixin, ch.ValueChannelMixin, ch.DatumChannelMixin
return {
c: c._encoding_name
for c in ch.__dict__.values()
if isinstance(c, type) and issubclass(c, mixins) and issubclass(c, SchemaBase)
}
def _invert_group_channels(
m: dict[type[SchemaBase], str], /
) -> dict[str, dict[_ChannelType, type[SchemaBase]]]:
"""Grouped inverted index for `_ChannelCache.channel_to_name`."""
def _reduce(it: Iterator[tuple[type[Any], str]]) -> Any:
"""
Returns a 1-2 item dict, per channel.
Never includes `datum`, as it is never utilized in `wrap_in_channel`.
"""
item: dict[Any, type[SchemaBase]] = {}
for tp, _ in it:
name = tp.__name__
if name.endswith("Datum"):
continue
elif name.endswith("Value"):
sub_key = "value"
else:
sub_key = "field"
item[sub_key] = tp
return item
grouper = groupby(m.items(), itemgetter(1))
return {k: _reduce(chans) for k, chans in grouper}
def infer_encoding_types(args: tuple[Any, ...], kwargs: dict[str, Any]):
"""
Infer typed keyword arguments for args and kwargs.
Parameters
----------
args : Sequence
Sequence of function args
kwargs : MutableMapping
Dict of function kwargs
Returns
-------
kwargs : dict
All args and kwargs in a single dict, with keys and types
based on the channels mapping.
"""
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
encoding = cache.get_encoding(type(el))
if encoding not in kwargs:
kwargs[encoding] = arg
else:
msg = f"encoding {encoding!r} specified twice."
raise ValueError(msg)
return cache.infer_encoding_types(kwargs)

View File

@@ -0,0 +1,442 @@
from __future__ import annotations
import hashlib
import json
import random
import sys
from collections.abc import MutableMapping, Sequence
from functools import partial
from pathlib import Path
from typing import (
TYPE_CHECKING,
Any,
Callable,
Literal,
TypedDict,
TypeVar,
Union,
overload,
)
import narwhals.stable.v1 as nw
from narwhals.stable.v1.dependencies import is_pandas_dataframe
from narwhals.stable.v1.typing import IntoDataFrame
from ._importers import import_pyarrow_interchange
from .core import (
DataFrameLike,
sanitize_geo_interface,
sanitize_narwhals_dataframe,
sanitize_pandas_dataframe,
to_eager_narwhals_dataframe,
)
from .plugin_registry import PluginRegistry
if sys.version_info >= (3, 13):
from typing import Protocol, runtime_checkable
else:
from typing_extensions import Protocol, runtime_checkable
if sys.version_info >= (3, 10):
from typing import Concatenate, ParamSpec
else:
from typing_extensions import Concatenate, ParamSpec
if TYPE_CHECKING:
if sys.version_info >= (3, 13):
from typing import TypeIs
else:
from typing_extensions import TypeIs
if sys.version_info >= (3, 10):
from typing import TypeAlias
else:
from typing_extensions import TypeAlias
import pandas as pd
import pyarrow as pa
@runtime_checkable
class SupportsGeoInterface(Protocol):
__geo_interface__: MutableMapping
DataType: TypeAlias = Union[
dict[Any, Any], IntoDataFrame, SupportsGeoInterface, DataFrameLike
]
TDataType = TypeVar("TDataType", bound=DataType)
TIntoDataFrame = TypeVar("TIntoDataFrame", bound=IntoDataFrame)
VegaLiteDataDict: TypeAlias = dict[
str, Union[str, dict[Any, Any], list[dict[Any, Any]]]
]
ToValuesReturnType: TypeAlias = dict[str, Union[dict[Any, Any], list[dict[Any, Any]]]]
SampleReturnType = Union[IntoDataFrame, dict[str, Sequence], None]
def is_data_type(obj: Any) -> TypeIs[DataType]:
return isinstance(obj, (dict, SupportsGeoInterface)) or isinstance(
nw.from_native(obj, eager_or_interchange_only=True, pass_through=True),
nw.DataFrame,
)
# ==============================================================================
# Data transformer registry
#
# A data transformer is a callable that takes a supported data type and returns
# a transformed dictionary version of it which is compatible with the VegaLite schema.
# The dict objects will be the Data portion of the VegaLite schema.
#
# Renderers only deal with the dict form of a
# VegaLite spec, after the Data model has been put into a schema compliant
# form.
# ==============================================================================
P = ParamSpec("P")
# NOTE: `Any` required due to the complexity of existing signatures imported in `altair.vegalite.v5.data.py`
R = TypeVar("R", VegaLiteDataDict, Any)
DataTransformerType = Callable[Concatenate[DataType, P], R]
class DataTransformerRegistry(PluginRegistry[DataTransformerType, R]):
_global_settings = {"consolidate_datasets": True}
@property
def consolidate_datasets(self) -> bool:
return self._global_settings["consolidate_datasets"]
@consolidate_datasets.setter
def consolidate_datasets(self, value: bool) -> None:
self._global_settings["consolidate_datasets"] = value
# ==============================================================================
class MaxRowsError(Exception):
"""Raised when a data model has too many rows."""
@overload
def limit_rows(data: None = ..., max_rows: int | None = ...) -> partial: ...
@overload
def limit_rows(data: DataType, max_rows: int | None = ...) -> DataType: ...
def limit_rows(
data: DataType | None = None, max_rows: int | None = 5000
) -> partial | DataType:
"""
Raise MaxRowsError if the data model has more than max_rows.
If max_rows is None, then do not perform any check.
"""
if data is None:
return partial(limit_rows, max_rows=max_rows)
check_data_type(data)
def raise_max_rows_error():
msg = (
"The number of rows in your dataset is greater "
f"than the maximum allowed ({max_rows}).\n\n"
"Try enabling the VegaFusion data transformer which "
"raises this limit by pre-evaluating data\n"
"transformations in Python.\n"
" >> import altair as alt\n"
' >> alt.data_transformers.enable("vegafusion")\n\n'
"Or, see https://altair-viz.github.io/user_guide/large_datasets.html "
"for additional information\n"
"on how to plot large datasets."
)
raise MaxRowsError(msg)
if isinstance(data, SupportsGeoInterface):
if data.__geo_interface__["type"] == "FeatureCollection":
values = data.__geo_interface__["features"]
else:
values = data.__geo_interface__
elif isinstance(data, dict):
if "values" in data:
values = data["values"]
else:
return data
else:
data = to_eager_narwhals_dataframe(data)
values = data
if max_rows is not None and len(values) > max_rows:
raise_max_rows_error()
return data
@overload
def sample(
data: None = ..., n: int | None = ..., frac: float | None = ...
) -> partial: ...
@overload
def sample(
data: TIntoDataFrame, n: int | None = ..., frac: float | None = ...
) -> TIntoDataFrame: ...
@overload
def sample(
data: DataType, n: int | None = ..., frac: float | None = ...
) -> SampleReturnType: ...
def sample(
data: DataType | None = None,
n: int | None = None,
frac: float | None = None,
) -> partial | SampleReturnType:
"""Reduce the size of the data model by sampling without replacement."""
if data is None:
return partial(sample, n=n, frac=frac)
check_data_type(data)
if is_pandas_dataframe(data):
return data.sample(n=n, frac=frac)
elif isinstance(data, dict):
if "values" in data:
values = data["values"]
if not n:
if frac is None:
msg = "frac cannot be None if n is None and data is a dictionary"
raise ValueError(msg)
n = int(frac * len(values))
values = random.sample(values, n)
return {"values": values}
else:
# Maybe this should raise an error or return something useful?
return None
data = nw.from_native(data, eager_only=True)
if not n:
if frac is None:
msg = "frac cannot be None if n is None with this data input type"
raise ValueError(msg)
n = int(frac * len(data))
indices = random.sample(range(len(data)), n)
return data[indices].to_native()
_FormatType = Literal["csv", "json"]
class _FormatDict(TypedDict):
type: _FormatType
class _ToFormatReturnUrlDict(TypedDict):
url: str
format: _FormatDict
@overload
def to_json(
data: None = ...,
prefix: str = ...,
extension: str = ...,
filename: str = ...,
urlpath: str = ...,
) -> partial: ...
@overload
def to_json(
data: DataType,
prefix: str = ...,
extension: str = ...,
filename: str = ...,
urlpath: str = ...,
) -> _ToFormatReturnUrlDict: ...
def to_json(
data: DataType | None = None,
prefix: str = "altair-data",
extension: str = "json",
filename: str = "{prefix}-{hash}.{extension}",
urlpath: str = "",
) -> partial | _ToFormatReturnUrlDict:
"""Write the data model to a .json file and return a url based data model."""
kwds = _to_text_kwds(prefix, extension, filename, urlpath)
if data is None:
return partial(to_json, **kwds)
else:
data_str = _data_to_json_string(data)
return _to_text(data_str, **kwds, format=_FormatDict(type="json"))
@overload
def to_csv(
data: None = ...,
prefix: str = ...,
extension: str = ...,
filename: str = ...,
urlpath: str = ...,
) -> partial: ...
@overload
def to_csv(
data: dict | pd.DataFrame | DataFrameLike,
prefix: str = ...,
extension: str = ...,
filename: str = ...,
urlpath: str = ...,
) -> _ToFormatReturnUrlDict: ...
def to_csv(
data: dict | pd.DataFrame | DataFrameLike | None = None,
prefix: str = "altair-data",
extension: str = "csv",
filename: str = "{prefix}-{hash}.{extension}",
urlpath: str = "",
) -> partial | _ToFormatReturnUrlDict:
"""Write the data model to a .csv file and return a url based data model."""
kwds = _to_text_kwds(prefix, extension, filename, urlpath)
if data is None:
return partial(to_csv, **kwds)
else:
data_str = _data_to_csv_string(data)
return _to_text(data_str, **kwds, format=_FormatDict(type="csv"))
def _to_text(
data: str,
prefix: str,
extension: str,
filename: str,
urlpath: str,
format: _FormatDict,
) -> _ToFormatReturnUrlDict:
data_hash = _compute_data_hash(data)
filename = filename.format(prefix=prefix, hash=data_hash, extension=extension)
Path(filename).write_text(data, encoding="utf-8")
url = str(Path(urlpath, filename))
return _ToFormatReturnUrlDict({"url": url, "format": format})
def _to_text_kwds(prefix: str, extension: str, filename: str, urlpath: str, /) -> dict[str, str]: # fmt: skip
return {"prefix": prefix, "extension": extension, "filename": filename, "urlpath": urlpath} # fmt: skip
def to_values(data: DataType) -> ToValuesReturnType:
"""Replace a DataFrame by a data model with values."""
check_data_type(data)
# `pass_through=True` passes `data` through as-is if it is not a Narwhals object.
data_native = nw.to_native(data, pass_through=True)
if isinstance(data_native, SupportsGeoInterface):
return {"values": _from_geo_interface(data_native)}
elif is_pandas_dataframe(data_native):
data_native = sanitize_pandas_dataframe(data_native)
return {"values": data_native.to_dict(orient="records")}
elif isinstance(data_native, dict):
if "values" not in data_native:
msg = "values expected in data dict, but not present."
raise KeyError(msg)
return data_native
elif isinstance(data, nw.DataFrame):
data = sanitize_narwhals_dataframe(data)
return {"values": data.rows(named=True)}
else:
# Should never reach this state as tested by check_data_type
msg = f"Unrecognized data type: {type(data)}"
raise ValueError(msg)
def check_data_type(data: DataType) -> None:
if not is_data_type(data):
msg = f"Expected dict, DataFrame or a __geo_interface__ attribute, got: {type(data)}"
raise TypeError(msg)
# ==============================================================================
# Private utilities
# ==============================================================================
def _compute_data_hash(data_str: str) -> str:
return hashlib.sha256(data_str.encode()).hexdigest()[:32]
def _from_geo_interface(data: SupportsGeoInterface | Any) -> dict[str, Any]:
"""
Santize a ``__geo_interface__`` w/ pre-santize step for ``pandas`` if needed.
Notes
-----
Split out to resolve typing issues related to:
- Intersection types
- ``typing.TypeGuard``
- ``pd.DataFrame.__getattr__``
"""
if is_pandas_dataframe(data):
data = sanitize_pandas_dataframe(data)
return sanitize_geo_interface(data.__geo_interface__)
def _data_to_json_string(data: DataType) -> str:
"""Return a JSON string representation of the input data."""
check_data_type(data)
if isinstance(data, SupportsGeoInterface):
return json.dumps(_from_geo_interface(data))
elif is_pandas_dataframe(data):
data = sanitize_pandas_dataframe(data)
return data.to_json(orient="records", double_precision=15)
elif isinstance(data, dict):
if "values" not in data:
msg = "values expected in data dict, but not present."
raise KeyError(msg)
return json.dumps(data["values"], sort_keys=True)
try:
data_nw = nw.from_native(data, eager_only=True)
except TypeError as exc:
msg = "to_json only works with data expressed as a DataFrame or as a dict"
raise NotImplementedError(msg) from exc
data_nw = sanitize_narwhals_dataframe(data_nw)
return json.dumps(data_nw.rows(named=True))
def _data_to_csv_string(data: DataType) -> str:
"""Return a CSV string representation of the input data."""
check_data_type(data)
if isinstance(data, SupportsGeoInterface):
msg = (
f"to_csv does not yet work with data that "
f"is of type {type(SupportsGeoInterface).__name__!r}.\n"
f"See https://github.com/vega/altair/issues/3441"
)
raise NotImplementedError(msg)
elif is_pandas_dataframe(data):
data = sanitize_pandas_dataframe(data)
return data.to_csv(index=False)
elif isinstance(data, dict):
if "values" not in data:
msg = "values expected in data dict, but not present"
raise KeyError(msg)
try:
import pandas as pd
except ImportError as exc:
msg = "pandas is required to convert a dict to a CSV string"
raise ImportError(msg) from exc
return pd.DataFrame.from_dict(data["values"]).to_csv(index=False)
try:
data_nw = nw.from_native(data, eager_only=True)
except TypeError as exc:
msg = "to_csv only works with data expressed as a DataFrame or as a dict"
raise NotImplementedError(msg) from exc
return data_nw.write_csv()
def arrow_table_from_dfi_dataframe(dfi_df: DataFrameLike) -> pa.Table:
"""Convert a DataFrame Interchange Protocol compatible object to an Arrow Table."""
import pyarrow as pa
# First check if the dataframe object has a method to convert to arrow.
# Give this preference over the pyarrow from_dataframe function since the object
# has more control over the conversion, and may have broader compatibility.
# This is the case for Polars, which supports Date32 columns in direct conversion
# while pyarrow does not yet support this type in from_dataframe
for convert_method_name in ("arrow", "to_arrow", "to_arrow_table", "to_pyarrow"):
convert_method = getattr(dfi_df, convert_method_name, None)
if callable(convert_method):
result = convert_method()
if isinstance(result, pa.Table):
return result
pi = import_pyarrow_interchange()
return pi.from_dataframe(dfi_df)

View File

@@ -0,0 +1,196 @@
from __future__ import annotations
import sys
import threading
import warnings
from typing import TYPE_CHECKING, Literal
if sys.version_info >= (3, 13):
from warnings import deprecated as _deprecated
else:
from typing_extensions import deprecated as _deprecated
if TYPE_CHECKING:
if sys.version_info >= (3, 11):
from typing import LiteralString
else:
from typing_extensions import LiteralString
__all__ = [
"AltairDeprecationWarning",
"deprecated",
"deprecated_static_only",
"deprecated_warn",
]
class AltairDeprecationWarning(DeprecationWarning): ...
def _format_message(
version: LiteralString,
alternative: LiteralString | None,
message: LiteralString | None,
/,
) -> LiteralString:
output = f"\nDeprecated since `altair={version}`."
if alternative:
output = f"{output} Use {alternative} instead."
return f"{output}\n{message}" if message else output
# NOTE: Annotating the return type breaks `pyright` detecting [reportDeprecated]
# NOTE: `LiteralString` requirement is introduced by stubs
def deprecated(
*,
version: LiteralString,
alternative: LiteralString | None = None,
message: LiteralString | None = None,
category: type[AltairDeprecationWarning] | None = AltairDeprecationWarning,
stacklevel: int = 1,
): # te.deprecated
"""
Indicate that a class, function or overload is deprecated.
When this decorator is applied to an object, the type checker
will generate a diagnostic on usage of the deprecated object.
Parameters
----------
version
``altair`` version the deprecation first appeared.
alternative
Suggested replacement class/method/function.
message
Additional message appended to ``version``, ``alternative``.
category
If the *category* is ``None``, no warning is emitted at runtime.
stacklevel
The *stacklevel* determines where the
warning is emitted. If it is ``1`` (the default), the warning
is emitted at the direct caller of the deprecated object; if it
is higher, it is emitted further up the stack.
Static type checker behavior is not affected by the *category*
and *stacklevel* arguments.
References
----------
[PEP 702](https://peps.python.org/pep-0702/)
"""
msg = _format_message(version, alternative, message)
return _deprecated(msg, category=category, stacklevel=stacklevel)
def deprecated_warn(
message: LiteralString,
*,
version: LiteralString,
alternative: LiteralString | None = None,
category: type[AltairDeprecationWarning] = AltairDeprecationWarning,
stacklevel: int = 2,
action: Literal["once"] | None = None,
) -> None:
"""
Indicate that the current code path is deprecated.
This should be used for non-trivial cases *only*. ``@deprecated`` should
always be preferred as it is recognized by static type checkers.
Parameters
----------
message
Explanation of the deprecated behaviour.
.. note::
Unlike ``@deprecated``, this is *not* optional.
version
``altair`` version the deprecation first appeared.
alternative
Suggested replacement argument/method/function.
category
The runtime warning type emitted.
stacklevel
How far up the call stack to make this warning appear.
A value of ``2`` attributes the warning to the caller
of the code calling ``deprecated_warn()``.
References
----------
[warnings.warn](https://docs.python.org/3/library/warnings.html#warnings.warn)
"""
msg = _format_message(version, alternative, message)
if action is None:
warnings.warn(msg, category=category, stacklevel=stacklevel)
elif action == "once":
_warn_once(msg, category=category, stacklevel=stacklevel)
else:
raise NotImplementedError(action)
deprecated_static_only = _deprecated
"""
Using this decorator **exactly as described**, ensures ``message`` is displayed to a static type checker.
**BE CAREFUL USING THIS**.
See screenshots in `comment`_ for motivation.
Every use should look like::
@deprecated_static_only(
"Deprecated since `altair=5.5.0`. Use altair.other instead.",
category=None,
)
def old_function(*args): ...
If a runtime warning is desired, use `@alt.utils.deprecated` instead.
Parameters
----------
message : LiteralString
- **Not** a variable
- **Not** use placeholders
- **Not** use concatenation
- **Do not use anything that could be considered dynamic**
category : None
You **need** to explicitly pass ``None``
.. _comment:
https://github.com/vega/altair/pull/3618#issuecomment-2423991968
---
"""
class _WarningsMonitor:
def __init__(self) -> None:
self._warned: dict[LiteralString, Literal[True]] = {}
self._lock = threading.Lock()
def __contains__(self, key: LiteralString, /) -> bool:
with self._lock:
return key in self._warned
def hit(self, key: LiteralString, /) -> None:
with self._lock:
self._warned[key] = True
def clear(self) -> None:
with self._lock:
self._warned.clear()
_warnings_monitor = _WarningsMonitor()
def _warn_once(
msg: LiteralString, /, *, category: type[AltairDeprecationWarning], stacklevel: int
) -> None:
global _warnings_monitor
if msg in _warnings_monitor:
return
else:
_warnings_monitor.hit(msg)
warnings.warn(msg, category=category, stacklevel=stacklevel + 1)

View File

@@ -0,0 +1,232 @@
from __future__ import annotations
import json
import pkgutil
import textwrap
import uuid
from typing import TYPE_CHECKING, Any, Callable, Union
from ._vegafusion_data import compile_with_vegafusion, using_vegafusion
from .mimebundle import spec_to_mimebundle
from .plugin_registry import PluginEnabler, PluginRegistry
from .schemapi import validate_jsonschema
if TYPE_CHECKING:
import sys
if sys.version_info >= (3, 10):
from typing import TypeAlias
else:
from typing_extensions import TypeAlias
# ==============================================================================
# Renderer registry
# ==============================================================================
# MimeBundleType needs to be the same as what are acceptable return values
# for _repr_mimebundle_,
# see https://ipython.readthedocs.io/en/stable/config/integrating.html#MyObject._repr_mimebundle_
MimeBundleDataType: TypeAlias = dict[str, Any]
MimeBundleMetaDataType: TypeAlias = dict[str, Any]
MimeBundleType: TypeAlias = Union[
MimeBundleDataType, tuple[MimeBundleDataType, MimeBundleMetaDataType]
]
RendererType: TypeAlias = Callable[..., MimeBundleType]
# Subtype of MimeBundleType as more specific in the values of the dictionaries
DefaultRendererReturnType: TypeAlias = tuple[
dict[str, Union[str, dict[str, Any]]], dict[str, dict[str, Any]]
]
class RendererRegistry(PluginRegistry[RendererType, MimeBundleType]):
entrypoint_err_messages = {
"notebook": textwrap.dedent(
"""
To use the 'notebook' renderer, you must install the vega package
and the associated Jupyter extension.
See https://altair-viz.github.io/getting_started/installation.html
for more information.
"""
),
}
def set_embed_options(
self,
defaultStyle: bool | str | None = None,
renderer: str | None = None,
width: int | None = None,
height: int | None = None,
padding: int | None = None,
scaleFactor: float | None = None,
actions: bool | dict[str, bool] | None = None,
format_locale: str | dict | None = None,
time_format_locale: str | dict | None = None,
**kwargs,
) -> PluginEnabler:
"""
Set options for embeddings of Vega & Vega-Lite charts.
Options are fully documented at https://github.com/vega/vega-embed.
Similar to the `enable()` method, this can be used as either
a persistent global switch, or as a temporary local setting using
a context manager (i.e. a `with` statement).
Parameters
----------
defaultStyle : bool or string
Specify a default stylesheet for embed actions.
renderer : string
The renderer to use for the view. One of "canvas" (default) or "svg"
width : integer
The view width in pixels
height : integer
The view height in pixels
padding : integer
The view padding in pixels
scaleFactor : number
The number by which to multiply the width and height (default 1)
of an exported PNG or SVG image.
actions : bool or dict
Determines if action links ("Export as PNG/SVG", "View Source",
"View Vega" (only for Vega-Lite), "Open in Vega Editor") are
included with the embedded view. If the value is true, all action
links will be shown and none if the value is false. This property
can take a key-value mapping object that maps keys (export, source,
compiled, editor) to boolean values for determining if
each action link should be shown.
format_locale : str or dict
d3-format locale name or dictionary. Defaults to "en-US" for United States English.
See https://github.com/d3/d3-format/tree/main/locale for available names and example
definitions.
time_format_locale : str or dict
d3-time-format locale name or dictionary. Defaults to "en-US" for United States English.
See https://github.com/d3/d3-time-format/tree/main/locale for available names and example
definitions.
**kwargs :
Additional options are passed directly to embed options.
"""
options: dict[str, bool | str | float | dict[str, bool] | None] = {
"defaultStyle": defaultStyle,
"renderer": renderer,
"width": width,
"height": height,
"padding": padding,
"scaleFactor": scaleFactor,
"actions": actions,
"formatLocale": format_locale,
"timeFormatLocale": time_format_locale,
}
kwargs.update({key: val for key, val in options.items() if val is not None})
return self.enable(None, embed_options=kwargs)
# ==============================================================================
# VegaLite v1/v2 renderer logic
# ==============================================================================
class Displayable:
"""
A base display class for VegaLite v1/v2.
This class takes a VegaLite v1/v2 spec and does the following:
1. Optionally validates the spec against a schema.
2. Uses the RendererPlugin to grab a renderer and call it when the
IPython/Jupyter display method (_repr_mimebundle_) is called.
The spec passed to this class must be fully schema compliant and already
have the data portion of the spec fully processed and ready to serialize.
In practice, this means, the data portion of the spec should have been passed
through appropriate data model transformers.
"""
renderers: RendererRegistry | None = None
schema_path = ("altair", "")
def __init__(self, spec: dict[str, Any], validate: bool = False) -> None:
self.spec = spec
self.validate = validate
self._validate()
def _validate(self) -> None:
"""Validate the spec against the schema."""
data = pkgutil.get_data(*self.schema_path)
assert data is not None
schema_dict: dict[str, Any] = json.loads(data.decode("utf-8"))
validate_jsonschema(
self.spec,
schema_dict,
)
def _repr_mimebundle_(
self, include: Any = None, exclude: Any = None
) -> MimeBundleType:
"""Return a MIME bundle for display in Jupyter frontends."""
if self.renderers is not None:
renderer_func = self.renderers.get()
assert renderer_func is not None
return renderer_func(self.spec)
else:
return {}
def default_renderer_base(
spec: dict[str, Any], mime_type: str, str_repr: str, **options
) -> DefaultRendererReturnType:
"""
A default renderer for Vega or VegaLite that works for modern frontends.
This renderer works with modern frontends (JupyterLab, nteract) that know
how to render the custom VegaLite MIME type listed above.
"""
# Local import to avoid circular ImportError
from altair.vegalite.v5.display import VEGA_MIME_TYPE, VEGALITE_MIME_TYPE
assert isinstance(spec, dict)
bundle: dict[str, str | dict] = {}
metadata: dict[str, dict[str, Any]] = {}
if using_vegafusion():
spec = compile_with_vegafusion(spec)
# Swap mimetype from Vega-Lite to Vega.
# If mimetype was JSON, leave it alone
if mime_type == VEGALITE_MIME_TYPE:
mime_type = VEGA_MIME_TYPE
bundle[mime_type] = spec
bundle["text/plain"] = str_repr
if options:
metadata[mime_type] = options
return bundle, metadata
def json_renderer_base(
spec: dict[str, Any], str_repr: str, **options
) -> DefaultRendererReturnType:
"""
A renderer that returns a MIME type of application/json.
In JupyterLab/nteract this is rendered as a nice JSON tree.
"""
return default_renderer_base(
spec, mime_type="application/json", str_repr=str_repr, **options
)
class HTMLRenderer:
"""Object to render charts as HTML, with a unique output div each time."""
def __init__(self, output_div: str = "altair-viz-{}", **kwargs) -> None:
self._output_div = output_div
self.kwargs = kwargs
@property
def output_div(self) -> str:
return self._output_div.format(uuid.uuid4().hex)
def __call__(self, spec: dict[str, Any], **metadata) -> dict[str, str]:
kwargs = self.kwargs.copy()
kwargs.update(**metadata, output_div=self.output_div)
return spec_to_mimebundle(spec, format="html", **kwargs)

View File

@@ -0,0 +1,98 @@
from __future__ import annotations
import ast
import sys
from typing import TYPE_CHECKING, Any, Callable, Literal, overload
if TYPE_CHECKING:
from os import PathLike
from _typeshed import ReadableBuffer
if sys.version_info >= (3, 11):
from typing import Self
else:
from typing_extensions import Self
class _CatchDisplay:
"""Class to temporarily catch sys.displayhook."""
def __init__(self) -> None:
self.output: Any | None = None
def __enter__(self) -> Self:
self.old_hook: Callable[[object], Any] = sys.displayhook
sys.displayhook = self
return self
def __exit__(self, type, value, traceback) -> Literal[False]:
sys.displayhook = self.old_hook
# Returning False will cause exceptions to propagate
return False
def __call__(self, output: Any) -> None:
self.output = output
@overload
def eval_block(
code: str | Any,
namespace: dict[str, Any] | None = ...,
filename: str | ReadableBuffer | PathLike[Any] = ...,
*,
strict: Literal[False] = ...,
) -> Any | None: ...
@overload
def eval_block(
code: str | Any,
namespace: dict[str, Any] | None = ...,
filename: str | ReadableBuffer | PathLike[Any] = ...,
*,
strict: Literal[True] = ...,
) -> Any: ...
def eval_block(
code: str | Any,
namespace: dict[str, Any] | None = None,
filename: str | ReadableBuffer | PathLike[Any] = "<string>",
*,
strict: bool = False,
) -> Any | None:
"""
Execute a multi-line block of code in the given namespace.
If the final statement in the code is an expression, return
the result of the expression.
If ``strict``, raise a ``TypeError`` when the return value would be ``None``.
"""
tree = ast.parse(code, filename="<ast>", mode="exec")
if namespace is None:
namespace = {}
catch_display = _CatchDisplay()
if isinstance(tree.body[-1], ast.Expr):
to_exec, to_eval = tree.body[:-1], tree.body[-1:]
else:
to_exec, to_eval = tree.body, []
for node in to_exec:
compiled = compile(ast.Module([node], []), filename=filename, mode="exec")
exec(compiled, namespace)
with catch_display:
for node in to_eval:
compiled = compile(
ast.Interactive([node]), filename=filename, mode="single"
)
exec(compiled, namespace)
if strict:
output = catch_display.output
if output is None:
msg = f"Expected a non-None value but got {output!r}"
raise TypeError(msg)
else:
return output
else:
return catch_display.output

View File

@@ -0,0 +1,411 @@
from __future__ import annotations
import json
from typing import Any, Literal
import jinja2
from altair.utils._importers import import_vl_convert, vl_version_for_vl_convert
TemplateName = Literal["standard", "universal", "inline", "olli"]
RenderMode = Literal["vega", "vega-lite"]
HTML_TEMPLATE = jinja2.Template(
"""
{%- if fullhtml -%}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
{%- endif %}
<style>
#{{ output_div }}.vega-embed {
width: 100%;
display: flex;
}
#{{ output_div }}.vega-embed details,
#{{ output_div }}.vega-embed details summary {
position: relative;
}
</style>
{%- if not requirejs %}
<script type="text/javascript" src="{{ base_url }}/vega@{{ vega_version }}"></script>
{%- if mode == 'vega-lite' %}
<script type="text/javascript" src="{{ base_url }}/vega-lite@{{ vegalite_version }}"></script>
{%- endif %}
<script type="text/javascript" src="{{ base_url }}/vega-embed@{{ vegaembed_version }}"></script>
{%- endif %}
{%- if fullhtml %}
{%- if requirejs %}
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js"></script>
<script>
requirejs.config({
"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",
}
});
</script>
{%- endif %}
</head>
<body>
{%- endif %}
<div id="{{ output_div }}"></div>
<script>
{%- if requirejs and not fullhtml %}
requirejs.config({
"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",
}
});
{% endif %}
{% if requirejs -%}
require(['vega-embed'],
{%- else -%}
(
{%- endif -%}
function(vegaEmbed) {
var spec = {{ spec }};
var embedOpt = {{ embed_options }};
function showError(el, error){
el.innerHTML = ('<div style="color:red;">'
+ '<p>JavaScript Error: ' + error.message + '</p>'
+ "<p>This usually means there's a typo in your chart specification. "
+ "See the javascript console for the full traceback.</p>"
+ '</div>');
throw error;
}
const el = document.getElementById('{{ output_div }}');
vegaEmbed("#{{ output_div }}", spec, embedOpt)
.catch(error => showError(el, error));
}){% if not requirejs %}(vegaEmbed){% endif %};
</script>
{%- if fullhtml %}
</body>
</html>
{%- endif %}
"""
)
HTML_TEMPLATE_UNIVERSAL = jinja2.Template(
"""
<style>
#{{ output_div }}.vega-embed {
width: 100%;
display: flex;
}
#{{ output_div }}.vega-embed details,
#{{ output_div }}.vega-embed details summary {
position: relative;
}
</style>
<div id="{{ output_div }}"></div>
<script type="text/javascript">
var VEGA_DEBUG = (typeof VEGA_DEBUG == "undefined") ? {} : VEGA_DEBUG;
(function(spec, embedOpt){
let outputDiv = document.currentScript.previousElementSibling;
if (outputDiv.id !== "{{ output_div }}") {
outputDiv = document.getElementById("{{ output_div }}");
}
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",
};
function maybeLoadScript(lib, version) {
var key = `${lib.replace("-", "")}_version`;
return (VEGA_DEBUG[key] == version) ?
Promise.resolve(paths[lib]) :
new Promise(function(resolve, reject) {
var s = document.createElement('script');
document.getElementsByTagName("head")[0].appendChild(s);
s.async = true;
s.onload = () => {
VEGA_DEBUG[key] = version;
return resolve(paths[lib]);
};
s.onerror = () => reject(`Error loading script: ${paths[lib]}`);
s.src = paths[lib];
});
}
function showError(err) {
outputDiv.innerHTML = `<div class="error" style="color:red;">${err}</div>`;
throw err;
}
function displayChart(vegaEmbed) {
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(typeof define === "function" && define.amd) {
requirejs.config({paths});
let deps = ["vega-embed"];
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}}"))
.catch(showError)
.then(() => displayChart(vegaEmbed));
}
})({{ spec }}, {{ embed_options }});
</script>
"""
)
# This is like the HTML_TEMPLATE template, but includes vega javascript inline
# so that the resulting file is not dependent on external resources. This was
# ported over from altair_saver.
#
# implies requirejs=False and full_html=True
INLINE_HTML_TEMPLATE = jinja2.Template(
"""\
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
#{{ output_div }}.vega-embed {
width: 100%;
display: flex;
}
#{{ output_div }}.vega-embed details,
#{{ output_div }}.vega-embed details summary {
position: relative;
}
</style>
<script type="text/javascript">
// vega-embed.js bundle with Vega-Lite version v{{ vegalite_version }}
{{ vegaembed_script }}
</script>
</head>
<body>
<div class="vega-visualization" id="{{ output_div }}"></div>
<script type="text/javascript">
const spec = {{ spec }};
const embedOpt = {{ embed_options }};
vegaEmbed('#{{ output_div }}', spec, embedOpt).catch(console.error);
</script>
</body>
</html>
"""
)
HTML_TEMPLATE_OLLI = jinja2.Template(
"""
<style>
#{{ output_div }}.vega-embed {
width: 100%;
display: flex;
}
#{{ output_div }}.vega-embed details,
#{{ output_div }}.vega-embed details summary {
position: relative;
}
</style>
<div id="{{ output_div }}"></div>
<script type="text/javascript">
var VEGA_DEBUG = (typeof VEGA_DEBUG == "undefined") ? {} : VEGA_DEBUG;
(function(spec, embedOpt){
let outputDiv = document.currentScript.previousElementSibling;
if (outputDiv.id !== "{{ output_div }}") {
outputDiv = document.getElementById("{{ output_div }}");
}
const olliDiv = document.createElement("div");
const vegaDiv = document.createElement("div");
outputDiv.appendChild(vegaDiv);
outputDiv.appendChild(olliDiv);
outputDiv = vegaDiv;
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",
"olli": "{{ base_url }}/olli@{{ olli_version }}?noext",
"olli-adapters": "{{ base_url }}/olli-adapters@{{ olli_adapters_version }}?noext",
};
function maybeLoadScript(lib, version) {
var key = `${lib.replace("-", "")}_version`;
return (VEGA_DEBUG[key] == version) ?
Promise.resolve(paths[lib]) :
new Promise(function(resolve, reject) {
var s = document.createElement('script');
document.getElementsByTagName("head")[0].appendChild(s);
s.async = true;
s.onload = () => {
VEGA_DEBUG[key] = version;
return resolve(paths[lib]);
};
s.onerror = () => reject(`Error loading script: ${paths[lib]}`);
s.src = paths[lib];
});
}
function showError(err) {
outputDiv.innerHTML = `<div class="error" style="color:red;">${err}</div>`;
throw err;
}
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.`));
olliAdapters.VegaLiteAdapter(spec).then(olliVisSpec => {
const olliFunc = typeof olli === 'function' ? olli : olli.olli;
const olliRender = olliFunc(olliVisSpec);
olliDiv.append(olliRender);
});
}
if(typeof define === "function" && define.amd) {
requirejs.config({paths});
let deps = ["vega-embed", "olli", "olli-adapters"];
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}}"))
.then(() => maybeLoadScript("olli", "{{olli_version}}"))
.then(() => maybeLoadScript("olli-adapters", "{{olli_adapters_version}}"))
.catch(showError)
.then(() => displayChart(vegaEmbed, olli, OlliAdapters));
}
})({{ spec }}, {{ embed_options }});
</script>
"""
)
TEMPLATES: dict[TemplateName, jinja2.Template] = {
"standard": HTML_TEMPLATE,
"universal": HTML_TEMPLATE_UNIVERSAL,
"inline": INLINE_HTML_TEMPLATE,
"olli": HTML_TEMPLATE_OLLI,
}
def spec_to_html(
spec: dict[str, Any],
mode: RenderMode,
vega_version: str | None,
vegaembed_version: str | None,
vegalite_version: str | None = None,
base_url: str = "https://cdn.jsdelivr.net/npm",
output_div: str = "vis",
embed_options: dict[str, Any] | None = None,
json_kwds: dict[str, Any] | None = None,
fullhtml: bool = True,
requirejs: bool = False,
template: jinja2.Template | TemplateName = "standard",
) -> str:
"""
Embed a Vega/Vega-Lite spec into an HTML page.
Parameters
----------
spec : dict
a dictionary representing a vega-lite plot spec.
mode : string {'vega' | 'vega-lite'}
The rendering mode. This value is overridden by embed_options['mode'],
if it is present.
vega_version : string
For html output, the version of vega.js to use.
vegalite_version : string
For html output, the version of vegalite.js to use.
vegaembed_version : string
For html output, the version of vegaembed.js to use.
base_url : string (optional)
The base url from which to load the javascript libraries.
output_div : string (optional)
The id of the div element where the plot will be shown.
embed_options : dict (optional)
Dictionary of options to pass to the vega-embed script. Default
entry is {'mode': mode}.
json_kwds : dict (optional)
Dictionary of keywords to pass to json.dumps().
fullhtml : boolean (optional)
If True (default) then return a full html page. If False, then return
an HTML snippet that can be embedded into an HTML page.
requirejs : boolean (optional)
If False (default) then load libraries from base_url using <script>
tags. If True, then load libraries using requirejs
template : jinja2.Template or string (optional)
Specify the template to use (default = 'standard'). If template is a
string, it must be one of {'universal', 'standard', 'inline'}. Otherwise, it
can be a jinja2.Template object containing a custom template.
Returns
-------
output : string
an HTML string for rendering the chart.
"""
embed_options = embed_options or {}
json_kwds = json_kwds or {}
mode = embed_options.setdefault("mode", mode)
if mode not in {"vega", "vega-lite"}:
msg = "mode must be either 'vega' or 'vega-lite'"
raise ValueError(msg)
if vega_version is None:
msg = "must specify vega_version"
raise ValueError(msg)
if vegaembed_version is None:
msg = "must specify vegaembed_version"
raise ValueError(msg)
if mode == "vega-lite" and vegalite_version is None:
msg = "must specify vega-lite version for mode='vega-lite'"
raise ValueError(msg)
render_kwargs = {}
if template == "inline":
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
jinja_template = TEMPLATES.get(template, template) # type: ignore[arg-type]
if not hasattr(jinja_template, "render"):
msg = f"Invalid template: {jinja_template}"
raise ValueError(msg)
return jinja_template.render(
spec=json.dumps(spec, **json_kwds),
embed_options=json.dumps(embed_options),
mode=mode,
vega_version=vega_version,
vegalite_version=vegalite_version,
vegaembed_version=vegaembed_version,
base_url=base_url,
output_div=output_div,
fullhtml=fullhtml,
requirejs=requirejs,
**render_kwargs,
)

View File

@@ -0,0 +1,377 @@
from __future__ import annotations
import struct
from typing import TYPE_CHECKING, Any, Literal, cast, overload
from ._importers import import_vl_convert, vl_version_for_vl_convert
from .html import spec_to_html
if TYPE_CHECKING:
import sys
if sys.version_info >= (3, 10):
from typing import TypeAlias
else:
from typing_extensions import TypeAlias
MimeBundleFormat: TypeAlias = Literal[
"html", "json", "png", "svg", "pdf", "vega", "vega-lite"
]
@overload
def spec_to_mimebundle(
spec: dict[str, Any],
format: Literal["json", "vega-lite"],
mode: Literal["vega-lite"] | None = ...,
vega_version: str | None = ...,
vegaembed_version: str | None = ...,
vegalite_version: str | None = ...,
embed_options: dict[str, Any] | None = ...,
engine: Literal["vl-convert"] | None = ...,
**kwargs,
) -> dict[str, dict[str, Any]]: ...
@overload
def spec_to_mimebundle(
spec: dict[str, Any],
format: Literal["html"],
mode: Literal["vega-lite"] | None = ...,
vega_version: str | None = ...,
vegaembed_version: str | None = ...,
vegalite_version: str | None = ...,
embed_options: dict[str, Any] | None = ...,
engine: Literal["vl-convert"] | None = ...,
**kwargs,
) -> dict[str, str]: ...
@overload
def spec_to_mimebundle(
spec: dict[str, Any],
format: Literal["pdf", "svg", "vega"],
mode: Literal["vega-lite"] | None = ...,
vega_version: str | None = ...,
vegaembed_version: str | None = ...,
vegalite_version: str | None = ...,
embed_options: dict[str, Any] | None = ...,
engine: Literal["vl-convert"] | None = ...,
**kwargs,
) -> dict[str, Any]: ...
@overload
def spec_to_mimebundle(
spec: dict[str, Any],
format: Literal["png"],
mode: Literal["vega-lite"] | None = ...,
vega_version: str | None = ...,
vegaembed_version: str | None = ...,
vegalite_version: str | None = ...,
embed_options: dict[str, Any] | None = ...,
engine: Literal["vl-convert"] | None = ...,
**kwargs,
) -> tuple[dict[str, Any], dict[str, Any]]: ...
def spec_to_mimebundle(
spec: dict[str, Any],
format: MimeBundleFormat,
mode: Literal["vega-lite"] | None = None,
vega_version: str | None = None,
vegaembed_version: str | None = None,
vegalite_version: str | None = None,
embed_options: dict[str, Any] | None = None,
engine: Literal["vl-convert"] | None = None,
**kwargs,
) -> dict[str, Any] | tuple[dict[str, Any], dict[str, Any]]:
"""
Convert a vega-lite specification to a mimebundle.
The mimebundle type is controlled by the ``format`` argument, which can be
one of the following ['html', 'json', 'png', 'svg', 'pdf', 'vega', 'vega-lite']
Parameters
----------
spec : dict
a dictionary representing a vega-lite plot spec
format : string {'html', 'json', 'png', 'svg', 'pdf', 'vega', 'vega-lite'}
the file format to be saved.
mode : string {'vega-lite'}
The rendering mode.
vega_version : string
The version of vega.js to use
vegaembed_version : string
The version of vegaembed.js to use
vegalite_version : string
The version of vegalite.js to use. Only required if mode=='vega-lite'
embed_options : dict (optional)
The vegaEmbed options dictionary. Defaults to the embed options set with
alt.renderers.set_embed_options().
(See https://github.com/vega/vega-embed for details)
engine: string {'vl-convert'}
the conversion engine to use for 'png', 'svg', 'pdf', and 'vega' formats
**kwargs :
Additional arguments will be passed to the generating function
Returns
-------
output : dict
a mime-bundle representing the image
Note
----
The png, svg, pdf, and vega outputs require the vl-convert package
"""
# Local import to avoid circular ImportError
from altair import renderers
from altair.utils.display import compile_with_vegafusion, using_vegafusion
if mode != "vega-lite":
msg = "mode must be 'vega-lite'"
raise ValueError(msg)
internal_mode: Literal["vega-lite", "vega"] = mode
if using_vegafusion():
spec = compile_with_vegafusion(spec)
internal_mode = "vega"
# Default to the embed options set by alt.renderers.set_embed_options
if embed_options is None:
final_embed_options = renderers.options.get("embed_options", {})
else:
final_embed_options = embed_options
embed_options = preprocess_embed_options(final_embed_options)
if format in {"png", "svg", "pdf", "vega"}:
return _spec_to_mimebundle_with_engine(
spec,
cast(Literal["png", "svg", "pdf", "vega"], format),
internal_mode,
engine=engine,
format_locale=embed_options.get("formatLocale", None),
time_format_locale=embed_options.get("timeFormatLocale", None),
**kwargs,
)
elif format == "html":
html = spec_to_html(
spec,
mode=internal_mode,
vega_version=vega_version,
vegaembed_version=vegaembed_version,
vegalite_version=vegalite_version,
embed_options=embed_options,
**kwargs,
)
return {"text/html": html}
elif format == "vega-lite":
if vegalite_version is None:
msg = "Must specify vegalite_version"
raise ValueError(msg)
return {f"application/vnd.vegalite.v{vegalite_version[0]}+json": spec}
elif format == "json":
return {"application/json": spec}
else:
msg = (
"format must be one of "
"['html', 'json', 'png', 'svg', 'pdf', 'vega', 'vega-lite']"
)
raise ValueError(msg)
def _spec_to_mimebundle_with_engine(
spec: dict,
format: Literal["png", "svg", "pdf", "vega"],
mode: Literal["vega-lite", "vega"],
format_locale: str | dict | None = None,
time_format_locale: str | dict | None = None,
**kwargs,
) -> Any:
"""
Helper for Vega-Lite to mimebundle conversions that require an engine.
Parameters
----------
spec : dict
a dictionary representing a vega-lite plot spec
format : string {'png', 'svg', 'pdf', 'vega'}
the format of the mimebundle to be returned
mode : string {'vega-lite', 'vega'}
The rendering mode.
engine: string {'vl-convert'}
the conversion engine to use
format_locale : str or dict
d3-format locale name or dictionary. Defaults to "en-US" for United States English.
See https://github.com/d3/d3-format/tree/main/locale for available names and example
definitions.
time_format_locale : str or dict
d3-time-format locale name or dictionary. Defaults to "en-US" for United States English.
See https://github.com/d3/d3-time-format/tree/main/locale for available names and example
definitions.
**kwargs :
Additional arguments will be passed to the conversion function
"""
# Normalize the engine string (if any) by lower casing
# and removing underscores and hyphens
engine = kwargs.pop("engine", None)
normalized_engine = _validate_normalize_engine(engine, format)
if normalized_engine == "vlconvert":
vlc = import_vl_convert()
vl_version = vl_version_for_vl_convert()
if format == "vega":
if mode == "vega":
vg = spec
else:
vg = vlc.vegalite_to_vega(spec, vl_version=vl_version)
return {"application/vnd.vega.v5+json": vg}
elif format == "svg":
if mode == "vega":
svg = vlc.vega_to_svg(
spec,
format_locale=format_locale,
time_format_locale=time_format_locale,
)
else:
svg = vlc.vegalite_to_svg(
spec,
vl_version=vl_version,
format_locale=format_locale,
time_format_locale=time_format_locale,
)
return {"image/svg+xml": svg}
elif format == "png":
scale = kwargs.get("scale_factor", 1)
# The default ppi for a PNG file is 72
default_ppi = 72
ppi = kwargs.get("ppi", default_ppi)
if mode == "vega":
png = vlc.vega_to_png(
spec,
scale=scale,
ppi=ppi,
format_locale=format_locale,
time_format_locale=time_format_locale,
)
else:
png = vlc.vegalite_to_png(
spec,
vl_version=vl_version,
scale=scale,
ppi=ppi,
format_locale=format_locale,
time_format_locale=time_format_locale,
)
factor = ppi / default_ppi
w, h = _pngxy(png)
return {"image/png": png}, {
"image/png": {"width": w / factor, "height": h / factor}
}
elif format == "pdf":
scale = kwargs.get("scale_factor", 1)
if mode == "vega":
pdf = vlc.vega_to_pdf(
spec,
scale=scale,
format_locale=format_locale,
time_format_locale=time_format_locale,
)
else:
pdf = vlc.vegalite_to_pdf(
spec,
vl_version=vl_version,
scale=scale,
format_locale=format_locale,
time_format_locale=time_format_locale,
)
return {"application/pdf": pdf}
else:
# This should be validated above
# but raise exception for the sake of future development
msg = f"Unexpected format {format!r}"
raise ValueError(msg)
else:
# This should be validated above
# but raise exception for the sake of future development
msg = f"Unexpected normalized_engine {normalized_engine!r}"
raise ValueError(msg)
def _validate_normalize_engine(
engine: Literal["vl-convert"] | None,
format: Literal["png", "svg", "pdf", "vega"],
) -> str:
"""
Helper to validate and normalize the user-provided engine.
engine : {None, 'vl-convert'}
the user-provided engine string
format : string {'png', 'svg', 'pdf', 'vega'}
the format of the mimebundle to be returned
"""
# Try to import vl_convert
try:
vlc = import_vl_convert()
except ImportError:
vlc = None
# Normalize engine string by lower casing and removing underscores and hyphens
normalized_engine = (
None if engine is None else engine.lower().replace("-", "").replace("_", "")
)
# Validate or infer default value of normalized_engine
if normalized_engine == "vlconvert":
if vlc is None:
msg = "The 'vl-convert' conversion engine requires the vl-convert-python package"
raise ValueError(msg)
elif normalized_engine is None:
if vlc is not None:
normalized_engine = "vlconvert"
else:
msg = (
f"Saving charts in {format!r} format requires the vl-convert-python package: "
"see https://altair-viz.github.io/user_guide/saving_charts.html#png-svg-and-pdf-format"
)
raise ValueError(msg)
else:
msg = f"Invalid conversion engine {engine!r}. Expected vl-convert"
raise ValueError(msg)
return normalized_engine
def _pngxy(data):
"""
read the (width, height) from a PNG header.
Taken from IPython.display
"""
ihdr = data.index(b"IHDR")
# next 8 bytes are width/height
return struct.unpack(">ii", data[ihdr + 4 : ihdr + 12])
def preprocess_embed_options(embed_options: dict) -> dict:
"""
Preprocess embed options to a form compatible with Vega Embed.
Parameters
----------
embed_options : dict
The embed options dictionary to preprocess.
Returns
-------
embed_opts : dict
The preprocessed embed options dictionary.
"""
embed_options = (embed_options or {}).copy()
# Convert locale strings to objects compatible with Vega Embed using vl-convert
format_locale = embed_options.get("formatLocale", None)
if isinstance(format_locale, str):
vlc = import_vl_convert()
embed_options["formatLocale"] = vlc.get_format_locale(format_locale)
time_format_locale = embed_options.get("timeFormatLocale", None)
if isinstance(time_format_locale, str):
vlc = import_vl_convert()
embed_options["timeFormatLocale"] = vlc.get_time_format_locale(
time_format_locale
)
return embed_options

View File

@@ -0,0 +1,290 @@
from __future__ import annotations
import sys
from functools import partial
from importlib.metadata import entry_points
from typing import TYPE_CHECKING, Any, Callable, Generic, TypeVar, cast
from altair.utils.deprecation import deprecated_warn
if sys.version_info >= (3, 13):
from typing import TypeIs
else:
from typing_extensions import TypeIs
if sys.version_info >= (3, 12):
from typing import TypeAliasType
else:
from typing_extensions import TypeAliasType
if TYPE_CHECKING:
from types import TracebackType
T = TypeVar("T")
R = TypeVar("R")
Plugin = TypeAliasType("Plugin", Callable[..., R], type_params=(R,))
PluginT = TypeVar("PluginT", bound=Plugin[Any])
IsPlugin = Callable[[object], TypeIs[Plugin[Any]]]
def _is_type(tp: type[T], /) -> Callable[[object], TypeIs[type[T]]]:
"""
Converts a type to guard function.
Added for compatibility with original `PluginRegistry` default.
"""
def func(obj: object, /) -> TypeIs[type[T]]:
return isinstance(obj, tp)
return func
class NoSuchEntryPoint(Exception):
def __init__(self, group, name):
self.group = group
self.name = name
def __str__(self):
return f"No {self.name!r} entry point found in group {self.group!r}"
class PluginEnabler(Generic[PluginT, R]):
"""
Context manager for enabling plugins.
This object lets you use enable() as a context manager to
temporarily enable a given plugin::
with plugins.enable("name"):
do_something() # 'name' plugin temporarily enabled
# plugins back to original state
"""
def __init__(
self, registry: PluginRegistry[PluginT, R], name: str, **options: Any
) -> None:
self.registry: PluginRegistry[PluginT, R] = registry
self.name: str = name
self.options: dict[str, Any] = options
self.original_state: dict[str, Any] = registry._get_state()
self.registry._enable(name, **options)
def __enter__(self) -> PluginEnabler[PluginT, R]:
return self
def __exit__(self, typ: type, value: Exception, traceback: TracebackType) -> None:
self.registry._set_state(self.original_state)
def __repr__(self) -> str:
return f"{type(self.registry).__name__}.enable({self.name!r})"
class PluginRegistry(Generic[PluginT, R]):
"""
A registry for plugins.
This is a plugin registry that allows plugins to be loaded/registered
in two ways:
1. Through an explicit call to ``.register(name, value)``.
2. By looking for other Python packages that are installed and provide
a setuptools entry point group.
When you create an instance of this class, provide the name of the
entry point group to use::
reg = PluginRegister("my_entrypoint_group")
"""
# this is a mapping of name to error message to allow custom error messages
# in case an entrypoint is not found
entrypoint_err_messages: dict[str, str] = {}
# global settings is a key-value mapping of settings that are stored globally
# in the registry rather than passed to the plugins
_global_settings: dict[str, Any] = {}
def __init__(
self, entry_point_group: str = "", plugin_type: IsPlugin = callable
) -> None:
"""
Create a PluginRegistry for a named entry point group.
Parameters
----------
entry_point_group: str
The name of the entry point group.
plugin_type
A type narrowing function that will optionally be used for runtime
type checking loaded plugins.
References
----------
https://typing.readthedocs.io/en/latest/spec/narrowing.html
"""
self.entry_point_group: str = entry_point_group
self.plugin_type: IsPlugin
if plugin_type is not callable and isinstance(plugin_type, type):
msg: Any = (
f"Pass a callable `TypeIs` function to `plugin_type` instead.\n"
f"{type(self).__name__!r}(plugin_type)\n\n"
f"See also:\n"
f"https://typing.readthedocs.io/en/latest/spec/narrowing.html\n"
f"https://docs.astral.sh/ruff/rules/assert/"
)
deprecated_warn(msg, version="5.4.0")
self.plugin_type = cast(IsPlugin, _is_type(plugin_type))
else:
self.plugin_type = plugin_type
self._active: Plugin[R] | None = None
self._active_name: str = ""
self._plugins: dict[str, PluginT] = {}
self._options: dict[str, Any] = {}
self._global_settings: dict[str, Any] = self.__class__._global_settings.copy()
def register(self, name: str, value: PluginT | None) -> PluginT | None:
"""
Register a plugin by name and value.
This method is used for explicit registration of a plugin and shouldn't be
used to manage entry point managed plugins, which are auto-loaded.
Parameters
----------
name: str
The name of the plugin.
value: PluginType or None
The actual plugin object to register or None to unregister that plugin.
Returns
-------
plugin: PluginType or None
The plugin that was registered or unregistered.
"""
if value is None:
return self._plugins.pop(name, None)
elif self.plugin_type(value):
self._plugins[name] = value
return value
else:
msg = f"{type(value).__name__!r} is not compatible with {type(self).__name__!r}"
raise TypeError(msg)
def names(self) -> list[str]:
"""List the names of the registered and entry points plugins."""
exts = list(self._plugins.keys())
e_points = importlib_metadata_get(self.entry_point_group)
more_exts = [ep.name for ep in e_points]
exts.extend(more_exts)
return sorted(set(exts))
def _get_state(self) -> dict[str, Any]:
"""Return a dictionary representing the current state of the registry."""
return {
"_active": self._active,
"_active_name": self._active_name,
"_plugins": self._plugins.copy(),
"_options": self._options.copy(),
"_global_settings": self._global_settings.copy(),
}
def _set_state(self, state: dict[str, Any]) -> None:
"""Reset the state of the registry."""
assert set(state.keys()) == {
"_active",
"_active_name",
"_plugins",
"_options",
"_global_settings",
}
for key, val in state.items():
setattr(self, key, val)
def _enable(self, name: str, **options) -> None:
if name not in self._plugins:
try:
(ep,) = (
ep
for ep in importlib_metadata_get(self.entry_point_group)
if ep.name == name
)
except ValueError as err:
if name in self.entrypoint_err_messages:
raise ValueError(self.entrypoint_err_messages[name]) from err
else:
raise NoSuchEntryPoint(self.entry_point_group, name) from err
value = cast(PluginT, ep.load())
self.register(name, value)
self._active_name = name
self._active = self._plugins[name]
for key in set(options.keys()) & set(self._global_settings.keys()):
self._global_settings[key] = options.pop(key)
self._options = options
def enable(
self, name: str | None = None, **options: Any
) -> PluginEnabler[PluginT, R]:
"""
Enable a plugin by name.
This can be either called directly, or used as a context manager.
Parameters
----------
name : string (optional)
The name of the plugin to enable. If not specified, then use the
current active name.
**options :
Any additional parameters will be passed to the plugin as keyword
arguments
Returns
-------
PluginEnabler:
An object that allows enable() to be used as a context manager
"""
if name is None:
name = self.active
return PluginEnabler(self, name, **options)
@property
def active(self) -> str:
"""Return the name of the currently active plugin."""
return self._active_name
@property
def options(self) -> dict[str, Any]:
"""Return the current options dictionary."""
return self._options
def get(self) -> partial[R] | Plugin[R] | None:
"""Return the currently active plugin."""
if (func := self._active) and self.plugin_type(func):
return partial(func, **self._options) if self._options else func
elif self._active is not None:
msg = (
f"{type(self).__name__!r} requires all plugins to be callable objects, "
f"but {type(self._active).__name__!r} is not callable."
)
raise TypeError(msg)
elif TYPE_CHECKING:
# NOTE: The `None` return is implicit, but `mypy` isn't satisfied
# - `ruff` will factor out explicit `None` return
# - `pyright` has no issue
raise NotImplementedError
def __repr__(self) -> str:
return f"{type(self).__name__}(active={self.active!r}, registered={self.names()!r})"
def importlib_metadata_get(group):
ep = entry_points()
# 'select' was introduced in Python 3.10 and 'get' got deprecated
# We don't check for Python version here as by checking with hasattr we
# also get compatibility with the importlib_metadata package which had a different
# deprecation cycle for 'get'
if hasattr(ep, "select"):
return ep.select(group=group) # pyright: ignore
else:
return ep.get(group, [])

View File

@@ -0,0 +1,224 @@
from __future__ import annotations
import json
import pathlib
import warnings
from typing import IO, TYPE_CHECKING, Any, Literal
from altair.utils._vegafusion_data import using_vegafusion
from altair.utils.deprecation import deprecated_warn
from altair.vegalite.v5.data import data_transformers
from .mimebundle import spec_to_mimebundle
if TYPE_CHECKING:
from pathlib import Path
def write_file_or_filename(
fp: str | Path | IO,
content: str | bytes,
mode: str = "w",
encoding: str | None = None,
) -> None:
"""Write content to fp, whether fp is a string, a pathlib Path or a file-like object."""
if isinstance(fp, (str, pathlib.Path)):
with pathlib.Path(fp).open(mode=mode, encoding=encoding) as f:
f.write(content)
else:
fp.write(content)
def set_inspect_format_argument(
format: str | None, fp: str | Path | IO, inline: bool
) -> str:
"""Inspect the format argument in the save function."""
if format is None:
if isinstance(fp, (str, pathlib.Path)):
format = pathlib.Path(fp).suffix.lstrip(".")
else:
msg = (
"must specify file format: "
"['png', 'svg', 'pdf', 'html', 'json', 'vega']"
)
raise ValueError(msg)
if format != "html" and inline:
warnings.warn("inline argument ignored for non HTML formats.", stacklevel=1)
return format
def set_inspect_mode_argument(
mode: Literal["vega-lite"] | None,
embed_options: dict[str, Any],
spec: dict[str, Any],
vegalite_version: str | None,
) -> Literal["vega-lite"]:
"""Inspect the mode argument in the save function."""
if mode is None:
if "mode" in embed_options:
mode = embed_options["mode"]
elif "$schema" in spec:
mode = spec["$schema"].split("/")[-2]
else:
mode = "vega-lite"
if mode != "vega-lite":
msg = "mode must be 'vega-lite', " f"not '{mode}'"
raise ValueError(msg)
if mode == "vega-lite" and vegalite_version is None:
msg = "must specify vega-lite version"
raise ValueError(msg)
return mode
def save(
chart,
fp: str | Path | IO,
vega_version: str | None,
vegaembed_version: str | None,
format: Literal["json", "html", "png", "svg", "pdf"] | None = None,
mode: Literal["vega-lite"] | None = None,
vegalite_version: str | None = None,
embed_options: dict | None = None,
json_kwds: dict | None = None,
scale_factor: float = 1,
engine: Literal["vl-convert"] | None = None,
inline: bool = False,
**kwargs,
) -> None:
"""
Save a chart to file in a variety of formats.
Supported formats are [json, html, png, svg, pdf]
Parameters
----------
chart : alt.Chart
the chart instance to save
fp : string filename, pathlib.Path or file-like object
file to which to write the chart.
format : string (optional)
the format to write: one of ['json', 'html', 'png', 'svg', 'pdf'].
If not specified, the format will be determined from the filename.
mode : string (optional)
Must be 'vega-lite'. If not specified, then infer the mode from
the '$schema' property of the spec, or the ``opt`` dictionary.
If it's not specified in either of those places, then use 'vega-lite'.
vega_version : string (optional)
For html output, the version of vega.js to use
vegalite_version : string (optional)
For html output, the version of vegalite.js to use
vegaembed_version : string (optional)
For html output, the version of vegaembed.js to use
embed_options : dict (optional)
The vegaEmbed options dictionary. Default is {}
(See https://github.com/vega/vega-embed for details)
json_kwds : dict (optional)
Additional keyword arguments are passed to the output method
associated with the specified format.
scale_factor : float (optional)
scale_factor to use to change size/resolution of png or svg output
engine: string {'vl-convert'}
the conversion engine to use for 'png', 'svg', and 'pdf' formats
inline: bool (optional)
If False (default), the required JavaScript libraries are loaded
from a CDN location in the resulting html file.
If True, the required JavaScript libraries are inlined into the resulting
html file so that it will work without an internet connection.
The vl-convert-python package is required if True.
**kwargs :
additional kwargs passed to spec_to_mimebundle.
"""
if _ := kwargs.pop("webdriver", None):
deprecated_warn(
"The webdriver argument is not relevant for the new vl-convert engine which replaced altair_saver. "
"The argument will be removed in a future release.",
version="5.0.0",
)
json_kwds = json_kwds or {}
encoding = kwargs.get("encoding", "utf-8")
format = set_inspect_format_argument(format, fp, inline) # type: ignore[assignment]
def perform_save() -> None:
spec = chart.to_dict(context={"pre_transform": False})
inner_mode = set_inspect_mode_argument(
mode, embed_options or {}, spec, vegalite_version
)
if format == "json":
json_spec = json.dumps(spec, **json_kwds)
write_file_or_filename(fp, json_spec, mode="w", encoding=encoding)
elif format == "html":
if inline:
kwargs["template"] = "inline"
mb_html = spec_to_mimebundle(
spec=spec,
format=format,
mode=inner_mode,
vega_version=vega_version,
vegalite_version=vegalite_version,
vegaembed_version=vegaembed_version,
embed_options=embed_options,
json_kwds=json_kwds,
**kwargs,
)
write_file_or_filename(
fp, mb_html["text/html"], mode="w", encoding=encoding
)
elif format == "png":
mb_png = spec_to_mimebundle(
spec=spec,
format=format,
mode=inner_mode,
vega_version=vega_version,
vegalite_version=vegalite_version,
vegaembed_version=vegaembed_version,
embed_options=embed_options,
scale_factor=scale_factor,
engine=engine,
**kwargs,
)
write_file_or_filename(fp, mb_png[0]["image/png"], mode="wb")
elif format in {"svg", "pdf", "vega"}:
mb_any = spec_to_mimebundle(
spec=spec,
format=format,
mode=inner_mode,
vega_version=vega_version,
vegalite_version=vegalite_version,
vegaembed_version=vegaembed_version,
embed_options=embed_options,
scale_factor=scale_factor,
engine=engine,
**kwargs,
)
if format == "pdf":
write_file_or_filename(fp, mb_any["application/pdf"], mode="wb")
else:
write_file_or_filename(
fp, mb_any["image/svg+xml"], mode="w", encoding=encoding
)
else:
msg = f"Unsupported format: '{format}'"
raise ValueError(msg)
if using_vegafusion():
# When the vegafusion data transformer is enabled, transforms will be
# evaluated during save and the resulting data will be included in the
# vega specification that is saved.
with data_transformers.disable_max_rows():
perform_save()
else:
# Temporarily turn off any data transformers so that all data is inlined
# when calling chart.to_dict. This is relevant for vl-convert which cannot access
# local json files which could be created by a json data transformer. Furthermore,
# we don't exit the with statement until this function completed due to the issue
# described at https://github.com/vega/vl-convert/issues/31
with data_transformers.enable("default"), data_transformers.disable_max_rows():
perform_save()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,130 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, NewType
# Type representing the "{selection}_store" dataset that corresponds to a
# Vega-Lite selection
Store = NewType("Store", list[dict[str, Any]])
@dataclass(frozen=True, eq=True)
class IndexSelection:
"""
Represents the state of an alt.selection_point() when neither the fields nor encodings arguments are specified.
The value field is a list of zero-based indices into the
selected dataset.
Note: These indices only apply to the input DataFrame
for charts that do not include aggregations (e.g. a scatter chart).
"""
name: str
value: list[int]
store: Store
@staticmethod
def from_vega(name: str, signal: dict[str, dict] | None, store: Store):
"""
Construct an IndexSelection from the raw Vega signal and dataset values.
Parameters
----------
name: str
The selection's name
signal: dict or None
The value of the Vega signal corresponding to the selection
store: list
The value of the Vega dataset corresponding to the selection.
This dataset is named "{name}_store" in the Vega view.
Returns
-------
IndexSelection
"""
if signal is None:
indices = []
else:
points = signal.get("vlPoint", {}).get("or", [])
indices = [p["_vgsid_"] - 1 for p in points]
return IndexSelection(name=name, value=indices, store=store)
@dataclass(frozen=True, eq=True)
class PointSelection:
"""
Represents the state of an alt.selection_point() when the fields or encodings arguments are specified.
The value field is a list of dicts of the form:
[{"dim1": 1, "dim2": "A"}, {"dim1": 2, "dim2": "BB"}]
where "dim1" and "dim2" are dataset columns and the dict values
correspond to the specific selected values.
"""
name: str
value: list[dict[str, Any]]
store: Store
@staticmethod
def from_vega(name: str, signal: dict[str, dict] | None, store: Store):
"""
Construct a PointSelection from the raw Vega signal and dataset values.
Parameters
----------
name: str
The selection's name
signal: dict or None
The value of the Vega signal corresponding to the selection
store: list
The value of the Vega dataset corresponding to the selection.
This dataset is named "{name}_store" in the Vega view.
Returns
-------
PointSelection
"""
points = [] if signal is None else signal.get("vlPoint", {}).get("or", [])
return PointSelection(name=name, value=points, store=store)
@dataclass(frozen=True, eq=True)
class IntervalSelection:
"""
Represents the state of an alt.selection_interval().
The value field is a dict of the form:
{"dim1": [0, 10], "dim2": ["A", "BB", "CCC"]}
where "dim1" and "dim2" are dataset columns and the dict values
correspond to the selected range.
"""
name: str
value: dict[str, list]
store: Store
@staticmethod
def from_vega(name: str, signal: dict[str, list] | None, store: Store):
"""
Construct an IntervalSelection from the raw Vega signal and dataset values.
Parameters
----------
name: str
The selection's name
signal: dict or None
The value of the Vega signal corresponding to the selection
store: list
The value of the Vega dataset corresponding to the selection.
This dataset is named "{name}_store" in the Vega view.
Returns
-------
PointSelection
"""
if signal is None:
signal = {}
return IntervalSelection(name=name, value=signal, store=store)

View File

@@ -0,0 +1,151 @@
"""
A Simple server used to show altair graphics from a prompt or script.
This is adapted from the mpld3 package; see
https://github.com/mpld3/mpld3/blob/master/mpld3/_server.py
"""
import itertools
import random
import socket
import sys
import threading
import webbrowser
from http import server
from io import BytesIO as IO
JUPYTER_WARNING = """
Note: if you're in the Jupyter notebook, Chart.serve() is not the best
way to view plots. Consider using Chart.display().
You must interrupt the kernel to cancel this command.
"""
# Mock server used for testing
class MockRequest:
def makefile(self, *args, **kwargs):
return IO(b"GET /")
def sendall(self, response):
pass
class MockServer:
def __init__(self, ip_port, Handler):
Handler(MockRequest(), ip_port[0], self)
def serve_forever(self):
pass
def server_close(self):
pass
def generate_handler(html, files=None):
if files is None:
files = {}
class MyHandler(server.BaseHTTPRequestHandler):
def do_GET(self):
"""Respond to a GET request."""
if self.path == "/":
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
self.wfile.write(html.encode())
elif self.path in files:
content_type, content = files[self.path]
self.send_response(200)
self.send_header("Content-type", content_type)
self.end_headers()
self.wfile.write(content.encode())
else:
self.send_error(404)
return MyHandler
def find_open_port(ip, port, n=50):
"""Find an open port near the specified port."""
ports = itertools.chain(
(port + i for i in range(n)), (port + random.randint(-2 * n, 2 * n))
)
for port in ports:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
result = s.connect_ex((ip, port))
s.close()
if result != 0:
return port
msg = "no open ports found"
raise ValueError(msg)
def serve(
html,
ip="127.0.0.1",
port=8888,
n_retries=50,
files=None,
jupyter_warning=True,
open_browser=True,
http_server=None,
) -> None:
"""
Start a server serving the given HTML, and (optionally) open a browser.
Parameters
----------
html : string
HTML to serve
ip : string (default = '127.0.0.1')
ip address at which the HTML will be served.
port : int (default = 8888)
the port at which to serve the HTML
n_retries : int (default = 50)
the number of nearby ports to search if the specified port is in use.
files : dictionary (optional)
dictionary of extra content to serve
jupyter_warning : bool (optional)
if True (default), then print a warning if this is used within Jupyter
open_browser : bool (optional)
if True (default), then open a web browser to the given HTML
http_server : class (optional)
optionally specify an HTTPServer class to use for showing the
figure. The default is Python's basic HTTPServer.
"""
port = find_open_port(ip, port, n_retries)
Handler = generate_handler(html, files)
if http_server is None:
srvr = server.HTTPServer((ip, port), Handler)
else:
srvr = http_server((ip, port), Handler)
if jupyter_warning:
try:
__IPYTHON__ # type: ignore # noqa
except NameError:
pass
else:
print(JUPYTER_WARNING)
# Start the server
print(f"Serving to http://{ip}:{port}/ [Ctrl-C to exit]")
sys.stdout.flush()
if open_browser:
# Use a thread to open a web browser pointing to the server
def b():
return webbrowser.open(f"http://{ip}:{port}")
threading.Thread(target=b).start()
try:
srvr.serve_forever()
except (KeyboardInterrupt, SystemExit):
print("\nstopping Server...")
srvr.server_close()

View File

@@ -0,0 +1,2 @@
# ruff: noqa: F403
from .v5 import *

View File

@@ -0,0 +1,2 @@
# ruff: noqa
from .v5.api import *

View File

@@ -0,0 +1,66 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Callable, overload
from altair.utils.core import sanitize_pandas_dataframe
from altair.utils.data import DataTransformerRegistry as _DataTransformerRegistry
from altair.utils.data import (
MaxRowsError,
check_data_type,
limit_rows,
sample,
to_csv,
to_json,
to_values,
)
if TYPE_CHECKING:
from altair.utils.data import DataType, ToValuesReturnType
from altair.utils.plugin_registry import PluginEnabler
@overload
def default_data_transformer(
data: None = ..., max_rows: int = ...
) -> Callable[[DataType], ToValuesReturnType]: ...
@overload
def default_data_transformer(
data: DataType, max_rows: int = ...
) -> ToValuesReturnType: ...
def default_data_transformer(
data: DataType | None = None, max_rows: int = 5000
) -> Callable[[DataType], ToValuesReturnType] | ToValuesReturnType:
if data is None:
def pipe(data: DataType, /) -> ToValuesReturnType:
data = limit_rows(data, max_rows=max_rows)
return to_values(data)
return pipe
else:
return to_values(limit_rows(data, max_rows=max_rows))
class DataTransformerRegistry(_DataTransformerRegistry):
def disable_max_rows(self) -> PluginEnabler:
"""Disable the MaxRowsError."""
options = self.options
if self.active in {"default", "vegafusion"}:
options = options.copy()
options["max_rows"] = None
return self.enable(**options)
__all__ = (
"DataTransformerRegistry",
"MaxRowsError",
"check_data_type",
"default_data_transformer",
"limit_rows",
"sample",
"sanitize_pandas_dataframe",
"to_csv",
"to_json",
"to_values",
)

View File

@@ -0,0 +1,17 @@
from altair.utils.display import (
DefaultRendererReturnType,
Displayable,
HTMLRenderer,
RendererRegistry,
default_renderer_base,
json_renderer_base,
)
__all__ = (
"DefaultRendererReturnType",
"Displayable",
"HTMLRenderer",
"RendererRegistry",
"default_renderer_base",
"json_renderer_base",
)

View File

@@ -0,0 +1,4 @@
"""Altair schema wrappers."""
# ruff: noqa: F403
from .v5.schema import *

View File

@@ -0,0 +1,652 @@
# ruff: noqa: F401, F403, F405
from altair.expr.core import datum
from altair.vegalite.v5 import api, compiler, schema
from altair.vegalite.v5.api import *
from altair.vegalite.v5.compiler import vegalite_compilers
from altair.vegalite.v5.data import (
MaxRowsError,
data_transformers,
default_data_transformer,
limit_rows,
sample,
to_csv,
to_json,
to_values,
)
from altair.vegalite.v5.display import (
VEGA_VERSION,
VEGAEMBED_VERSION,
VEGALITE_VERSION,
VegaLite,
renderers,
)
from altair.vegalite.v5.schema import *
# The content of __all__ is automatically written by
# tools/update_init_file.py. Do not modify directly.
__all__ = [
"SCHEMA_URL",
"SCHEMA_VERSION",
"TOPLEVEL_ONLY_KEYS",
"URI",
"VEGAEMBED_VERSION",
"VEGALITE_VERSION",
"VEGA_VERSION",
"X2",
"Y2",
"Aggregate",
"AggregateOp",
"AggregateTransform",
"AggregatedFieldDef",
"Align",
"AllSortString",
"Angle",
"AngleDatum",
"AngleValue",
"AnyMark",
"AnyMarkConfig",
"AreaConfig",
"ArgmaxDef",
"ArgminDef",
"AutoSizeParams",
"AutosizeType",
"Axis",
"AxisConfig",
"AxisOrient",
"AxisResolveMap",
"BBox",
"BarConfig",
"BaseTitleNoValueRefs",
"Baseline",
"Bin",
"BinExtent",
"BinParams",
"BinTransform",
"BindCheckbox",
"BindDirect",
"BindInput",
"BindRadioSelect",
"BindRange",
"Binding",
"BinnedTimeUnit",
"Blend",
"BoxPlot",
"BoxPlotConfig",
"BoxPlotDef",
"BrushConfig",
"CalculateTransform",
"Categorical",
"ChainedWhen",
"Chart",
"ChartDataType",
"Color",
"ColorDatum",
"ColorDef",
"ColorName",
"ColorScheme",
"ColorValue",
"Column",
"CompositeMark",
"CompositeMarkDef",
"CompositionConfig",
"ConcatChart",
"ConcatSpecGenericSpec",
"ConditionalAxisColor",
"ConditionalAxisLabelAlign",
"ConditionalAxisLabelBaseline",
"ConditionalAxisLabelFontStyle",
"ConditionalAxisLabelFontWeight",
"ConditionalAxisNumber",
"ConditionalAxisNumberArray",
"ConditionalAxisPropertyAlignnull",
"ConditionalAxisPropertyColornull",
"ConditionalAxisPropertyFontStylenull",
"ConditionalAxisPropertyFontWeightnull",
"ConditionalAxisPropertyTextBaselinenull",
"ConditionalAxisPropertynumberArraynull",
"ConditionalAxisPropertynumbernull",
"ConditionalAxisPropertystringnull",
"ConditionalAxisString",
"ConditionalMarkPropFieldOrDatumDef",
"ConditionalMarkPropFieldOrDatumDefTypeForShape",
"ConditionalParameterMarkPropFieldOrDatumDef",
"ConditionalParameterMarkPropFieldOrDatumDefTypeForShape",
"ConditionalParameterStringFieldDef",
"ConditionalParameterValueDefGradientstringnullExprRef",
"ConditionalParameterValueDefTextExprRef",
"ConditionalParameterValueDefnumber",
"ConditionalParameterValueDefnumberArrayExprRef",
"ConditionalParameterValueDefnumberExprRef",
"ConditionalParameterValueDefstringExprRef",
"ConditionalParameterValueDefstringnullExprRef",
"ConditionalPredicateMarkPropFieldOrDatumDef",
"ConditionalPredicateMarkPropFieldOrDatumDefTypeForShape",
"ConditionalPredicateStringFieldDef",
"ConditionalPredicateValueDefAlignnullExprRef",
"ConditionalPredicateValueDefColornullExprRef",
"ConditionalPredicateValueDefFontStylenullExprRef",
"ConditionalPredicateValueDefFontWeightnullExprRef",
"ConditionalPredicateValueDefGradientstringnullExprRef",
"ConditionalPredicateValueDefTextBaselinenullExprRef",
"ConditionalPredicateValueDefTextExprRef",
"ConditionalPredicateValueDefnumber",
"ConditionalPredicateValueDefnumberArrayExprRef",
"ConditionalPredicateValueDefnumberArraynullExprRef",
"ConditionalPredicateValueDefnumberExprRef",
"ConditionalPredicateValueDefnumbernullExprRef",
"ConditionalPredicateValueDefstringExprRef",
"ConditionalPredicateValueDefstringnullExprRef",
"ConditionalStringFieldDef",
"ConditionalValueDefGradientstringnullExprRef",
"ConditionalValueDefTextExprRef",
"ConditionalValueDefnumber",
"ConditionalValueDefnumberArrayExprRef",
"ConditionalValueDefnumberExprRef",
"ConditionalValueDefstringExprRef",
"ConditionalValueDefstringnullExprRef",
"Config",
"CsvDataFormat",
"Cursor",
"Cyclical",
"Data",
"DataFormat",
"DataSource",
"DataType",
"Datasets",
"DateTime",
"DatumChannelMixin",
"DatumDef",
"Day",
"DensityTransform",
"DerivedStream",
"Description",
"DescriptionValue",
"Detail",
"DictInlineDataset",
"DictSelectionInit",
"DictSelectionInitInterval",
"Diverging",
"DomainUnionWith",
"DsvDataFormat",
"Element",
"Encoding",
"EncodingSortField",
"ErrorBand",
"ErrorBandConfig",
"ErrorBandDef",
"ErrorBar",
"ErrorBarConfig",
"ErrorBarDef",
"ErrorBarExtent",
"EventStream",
"EventType",
"Expr",
"ExprRef",
"ExtentTransform",
"Facet",
"FacetChart",
"FacetEncodingFieldDef",
"FacetFieldDef",
"FacetMapping",
"FacetSpec",
"FacetedEncoding",
"FacetedUnitSpec",
"Feature",
"FeatureCollection",
"FeatureGeometryGeoJsonProperties",
"Field",
"FieldChannelMixin",
"FieldDefWithoutScale",
"FieldEqualPredicate",
"FieldGTEPredicate",
"FieldGTPredicate",
"FieldLTEPredicate",
"FieldLTPredicate",
"FieldName",
"FieldOneOfPredicate",
"FieldOrDatumDefWithConditionDatumDefGradientstringnull",
"FieldOrDatumDefWithConditionDatumDefnumber",
"FieldOrDatumDefWithConditionDatumDefnumberArray",
"FieldOrDatumDefWithConditionDatumDefstringnull",
"FieldOrDatumDefWithConditionMarkPropFieldDefGradientstringnull",
"FieldOrDatumDefWithConditionMarkPropFieldDefTypeForShapestringnull",
"FieldOrDatumDefWithConditionMarkPropFieldDefnumber",
"FieldOrDatumDefWithConditionMarkPropFieldDefnumberArray",
"FieldOrDatumDefWithConditionStringDatumDefText",
"FieldOrDatumDefWithConditionStringFieldDefText",
"FieldOrDatumDefWithConditionStringFieldDefstring",
"FieldRange",
"FieldRangePredicate",
"FieldValidPredicate",
"Fill",
"FillDatum",
"FillOpacity",
"FillOpacityDatum",
"FillOpacityValue",
"FillValue",
"FilterTransform",
"Fit",
"FlattenTransform",
"FoldTransform",
"FontStyle",
"FontWeight",
"FormatConfig",
"Generator",
"GenericUnitSpecEncodingAnyMark",
"GeoJsonFeature",
"GeoJsonFeatureCollection",
"GeoJsonProperties",
"Geometry",
"GeometryCollection",
"Gradient",
"GradientStop",
"GraticuleGenerator",
"GraticuleParams",
"HConcatChart",
"HConcatSpecGenericSpec",
"Header",
"HeaderConfig",
"HexColor",
"Href",
"HrefValue",
"Impute",
"ImputeMethod",
"ImputeParams",
"ImputeSequence",
"ImputeTransform",
"InlineData",
"InlineDataset",
"Interpolate",
"IntervalSelectionConfig",
"IntervalSelectionConfigWithoutType",
"JoinAggregateFieldDef",
"JoinAggregateTransform",
"JsonDataFormat",
"Key",
"LabelOverlap",
"LatLongDef",
"LatLongFieldDef",
"Latitude",
"Latitude2",
"Latitude2Datum",
"Latitude2Value",
"LatitudeDatum",
"LayerChart",
"LayerRepeatMapping",
"LayerRepeatSpec",
"LayerSpec",
"LayoutAlign",
"Legend",
"LegendBinding",
"LegendConfig",
"LegendOrient",
"LegendResolveMap",
"LegendStreamBinding",
"LineConfig",
"LineString",
"LinearGradient",
"LocalMultiTimeUnit",
"LocalSingleTimeUnit",
"Locale",
"LoessTransform",
"LogicalAndPredicate",
"LogicalNotPredicate",
"LogicalOrPredicate",
"Longitude",
"Longitude2",
"Longitude2Datum",
"Longitude2Value",
"LongitudeDatum",
"LookupData",
"LookupSelection",
"LookupTransform",
"Mark",
"MarkConfig",
"MarkDef",
"MarkInvalidDataMode",
"MarkPropDefGradientstringnull",
"MarkPropDefnumber",
"MarkPropDefnumberArray",
"MarkPropDefstringnullTypeForShape",
"MarkType",
"MaxRowsError",
"MergedStream",
"Month",
"MultiLineString",
"MultiPoint",
"MultiPolygon",
"MultiTimeUnit",
"NamedData",
"NonArgAggregateOp",
"NonLayerRepeatSpec",
"NonNormalizedSpec",
"NumberLocale",
"NumericArrayMarkPropDef",
"NumericMarkPropDef",
"OffsetDef",
"Opacity",
"OpacityDatum",
"OpacityValue",
"Order",
"OrderFieldDef",
"OrderOnlyDef",
"OrderValue",
"OrderValueDef",
"Orient",
"Orientation",
"OverlayMarkDef",
"Padding",
"Parameter",
"ParameterExpression",
"ParameterExtent",
"ParameterName",
"ParameterPredicate",
"Parse",
"ParseValue",
"PivotTransform",
"Point",
"PointSelectionConfig",
"PointSelectionConfigWithoutType",
"PolarDef",
"Polygon",
"Position",
"Position2Def",
"PositionDatumDef",
"PositionDatumDefBase",
"PositionDef",
"PositionFieldDef",
"PositionFieldDefBase",
"PositionValueDef",
"Predicate",
"PredicateComposition",
"PrimitiveValue",
"Projection",
"ProjectionConfig",
"ProjectionType",
"QuantileTransform",
"RadialGradient",
"Radius",
"Radius2",
"Radius2Datum",
"Radius2Value",
"RadiusDatum",
"RadiusValue",
"RangeConfig",
"RangeEnum",
"RangeRaw",
"RangeRawArray",
"RangeScheme",
"RectConfig",
"RegressionTransform",
"RelativeBandSize",
"RepeatChart",
"RepeatMapping",
"RepeatRef",
"RepeatSpec",
"Resolve",
"ResolveMode",
"Root",
"Row",
"RowColLayoutAlign",
"RowColboolean",
"RowColnumber",
"RowColumnEncodingFieldDef",
"SampleTransform",
"Scale",
"ScaleBinParams",
"ScaleBins",
"ScaleConfig",
"ScaleDatumDef",
"ScaleFieldDef",
"ScaleInterpolateEnum",
"ScaleInterpolateParams",
"ScaleInvalidDataConfig",
"ScaleInvalidDataShowAsValueangle",
"ScaleInvalidDataShowAsValuecolor",
"ScaleInvalidDataShowAsValuefill",
"ScaleInvalidDataShowAsValuefillOpacity",
"ScaleInvalidDataShowAsValueopacity",
"ScaleInvalidDataShowAsValueradius",
"ScaleInvalidDataShowAsValueshape",
"ScaleInvalidDataShowAsValuesize",
"ScaleInvalidDataShowAsValuestroke",
"ScaleInvalidDataShowAsValuestrokeDash",
"ScaleInvalidDataShowAsValuestrokeOpacity",
"ScaleInvalidDataShowAsValuestrokeWidth",
"ScaleInvalidDataShowAsValuetheta",
"ScaleInvalidDataShowAsValuex",
"ScaleInvalidDataShowAsValuexOffset",
"ScaleInvalidDataShowAsValuey",
"ScaleInvalidDataShowAsValueyOffset",
"ScaleInvalidDataShowAsangle",
"ScaleInvalidDataShowAscolor",
"ScaleInvalidDataShowAsfill",
"ScaleInvalidDataShowAsfillOpacity",
"ScaleInvalidDataShowAsopacity",
"ScaleInvalidDataShowAsradius",
"ScaleInvalidDataShowAsshape",
"ScaleInvalidDataShowAssize",
"ScaleInvalidDataShowAsstroke",
"ScaleInvalidDataShowAsstrokeDash",
"ScaleInvalidDataShowAsstrokeOpacity",
"ScaleInvalidDataShowAsstrokeWidth",
"ScaleInvalidDataShowAstheta",
"ScaleInvalidDataShowAsx",
"ScaleInvalidDataShowAsxOffset",
"ScaleInvalidDataShowAsy",
"ScaleInvalidDataShowAsyOffset",
"ScaleResolveMap",
"ScaleType",
"SchemaBase",
"SchemeParams",
"SecondaryFieldDef",
"SelectionConfig",
"SelectionExpression",
"SelectionInit",
"SelectionInitInterval",
"SelectionInitIntervalMapping",
"SelectionInitMapping",
"SelectionParameter",
"SelectionPredicateComposition",
"SelectionResolution",
"SelectionType",
"SequenceGenerator",
"SequenceParams",
"SequentialMultiHue",
"SequentialSingleHue",
"Shape",
"ShapeDatum",
"ShapeDef",
"ShapeValue",
"SharedEncoding",
"SingleDefUnitChannel",
"SingleTimeUnit",
"Size",
"SizeDatum",
"SizeValue",
"Sort",
"SortArray",
"SortByChannel",
"SortByChannelDesc",
"SortByEncoding",
"SortField",
"SortOrder",
"Spec",
"SphereGenerator",
"StackOffset",
"StackTransform",
"StandardType",
"Step",
"StepFor",
"Stream",
"StringFieldDef",
"StringFieldDefWithCondition",
"StringValueDefWithCondition",
"Stroke",
"StrokeCap",
"StrokeDash",
"StrokeDashDatum",
"StrokeDashValue",
"StrokeDatum",
"StrokeJoin",
"StrokeOpacity",
"StrokeOpacityDatum",
"StrokeOpacityValue",
"StrokeValue",
"StrokeWidth",
"StrokeWidthDatum",
"StrokeWidthValue",
"StyleConfigIndex",
"SymbolShape",
"Text",
"TextBaseline",
"TextDatum",
"TextDef",
"TextDirection",
"TextValue",
"Then",
"Theta",
"Theta2",
"Theta2Datum",
"Theta2Value",
"ThetaDatum",
"ThetaValue",
"TickConfig",
"TickCount",
"TimeInterval",
"TimeIntervalStep",
"TimeLocale",
"TimeUnit",
"TimeUnitParams",
"TimeUnitTransform",
"TimeUnitTransformParams",
"Title",
"TitleAnchor",
"TitleConfig",
"TitleFrame",
"TitleOrient",
"TitleParams",
"Tooltip",
"TooltipContent",
"TooltipValue",
"TopLevelConcatSpec",
"TopLevelFacetSpec",
"TopLevelHConcatSpec",
"TopLevelLayerSpec",
"TopLevelMixin",
"TopLevelParameter",
"TopLevelRepeatSpec",
"TopLevelSelectionParameter",
"TopLevelSpec",
"TopLevelUnitSpec",
"TopLevelVConcatSpec",
"TopoDataFormat",
"Transform",
"Type",
"TypeForShape",
"TypedFieldDef",
"UnitSpec",
"UnitSpecWithFrame",
"Url",
"UrlData",
"UrlValue",
"UtcMultiTimeUnit",
"UtcSingleTimeUnit",
"VConcatChart",
"VConcatSpecGenericSpec",
"ValueChannelMixin",
"ValueDefWithConditionMarkPropFieldOrDatumDefGradientstringnull",
"ValueDefWithConditionMarkPropFieldOrDatumDefTypeForShapestringnull",
"ValueDefWithConditionMarkPropFieldOrDatumDefnumber",
"ValueDefWithConditionMarkPropFieldOrDatumDefnumberArray",
"ValueDefWithConditionMarkPropFieldOrDatumDefstringnull",
"ValueDefWithConditionStringFieldDefText",
"ValueDefnumber",
"ValueDefnumberwidthheightExprRef",
"VariableParameter",
"Vector2DateTime",
"Vector2Vector2number",
"Vector2boolean",
"Vector2number",
"Vector2string",
"Vector3number",
"Vector7string",
"Vector10string",
"Vector12string",
"VegaLite",
"VegaLiteSchema",
"ViewBackground",
"ViewConfig",
"When",
"WindowEventType",
"WindowFieldDef",
"WindowOnlyOp",
"WindowTransform",
"X",
"X2Datum",
"X2Value",
"XDatum",
"XError",
"XError2",
"XError2Value",
"XErrorValue",
"XOffset",
"XOffsetDatum",
"XOffsetValue",
"XValue",
"Y",
"Y2Datum",
"Y2Value",
"YDatum",
"YError",
"YError2",
"YError2Value",
"YErrorValue",
"YOffset",
"YOffsetDatum",
"YOffsetValue",
"YValue",
"api",
"binding",
"binding_checkbox",
"binding_radio",
"binding_range",
"binding_select",
"channels",
"check_fields_and_encodings",
"compiler",
"concat",
"condition",
"core",
"data_transformers",
"datum",
"default_data_transformer",
"graticule",
"hconcat",
"layer",
"limit_rows",
"load_schema",
"mixins",
"param",
"renderers",
"repeat",
"sample",
"schema",
"selection",
"selection_interval",
"selection_multi",
"selection_point",
"selection_single",
"sequence",
"sphere",
"to_csv",
"to_json",
"to_values",
"topo_feature",
"value",
"vconcat",
"vegalite_compilers",
"when",
"with_property_setters",
]

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,23 @@
from typing import Final
from altair.utils._importers import import_vl_convert
from altair.utils.compiler import VegaLiteCompilerRegistry
ENTRY_POINT_GROUP: Final = "altair.vegalite.v5.vegalite_compiler"
vegalite_compilers = VegaLiteCompilerRegistry(entry_point_group=ENTRY_POINT_GROUP)
def vl_convert_compiler(vegalite_spec: dict) -> dict:
"""Vega-Lite to Vega compiler that uses vl-convert."""
from . import SCHEMA_VERSION
vlc = import_vl_convert()
# Compute vl-convert's vl_version string (of the form 'v5_8')
# from SCHEMA_VERSION (of the form 'v5.8.0')
vl_version = "_".join(SCHEMA_VERSION.split(".")[:2])
return vlc.vegalite_to_vega(vegalite_spec, vl_version=vl_version)
vegalite_compilers.register("vl-convert", vl_convert_compiler)
vegalite_compilers.enable("vl-convert")

View File

@@ -0,0 +1,41 @@
from typing import Final
from altair.utils._vegafusion_data import vegafusion_data_transformer
from altair.vegalite.data import (
DataTransformerRegistry,
MaxRowsError,
default_data_transformer,
limit_rows,
sample,
to_csv,
to_json,
to_values,
)
# ==============================================================================
# VegaLite 5 data transformers
# ==============================================================================
ENTRY_POINT_GROUP: Final = "altair.vegalite.v5.data_transformer"
data_transformers = DataTransformerRegistry(entry_point_group=ENTRY_POINT_GROUP)
data_transformers.register("default", default_data_transformer)
data_transformers.register("json", to_json)
# FIXME: `to_csv` cannot accept all `DataType` https://github.com/vega/altair/issues/3441
data_transformers.register("csv", to_csv) # type: ignore[arg-type]
data_transformers.register("vegafusion", vegafusion_data_transformer)
data_transformers.enable("default")
__all__ = (
"MaxRowsError",
"default_data_transformer",
"limit_rows",
"sample",
"to_csv",
"to_json",
"to_values",
"vegafusion_data_transformer",
)

View File

@@ -0,0 +1,191 @@
from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING, Final
from altair.utils.mimebundle import spec_to_mimebundle
from altair.vegalite.display import (
Displayable,
HTMLRenderer,
RendererRegistry,
default_renderer_base,
json_renderer_base,
)
from .schema import SCHEMA_VERSION
if TYPE_CHECKING:
from altair.vegalite.display import DefaultRendererReturnType
VEGALITE_VERSION: Final = SCHEMA_VERSION.lstrip("v")
VEGA_VERSION: Final = "5"
VEGAEMBED_VERSION: Final = "6"
# ==============================================================================
# VegaLite v5 renderer logic
# ==============================================================================
# The MIME type for Vega-Lite 5.x releases.
VEGALITE_MIME_TYPE: Final = "application/vnd.vegalite.v5+json"
# The MIME type for Vega 5.x releases.
VEGA_MIME_TYPE: Final = "application/vnd.vega.v5+json"
# The entry point group that can be used by other packages to declare other
# renderers that will be auto-detected. Explicit registration is also
# allowed by the PluginRegistery API.
ENTRY_POINT_GROUP: Final = "altair.vegalite.v5.renderer"
# The display message when rendering fails
DEFAULT_DISPLAY: Final = f"""\
<VegaLite {VEGALITE_VERSION.split('.')[0]} object>
If you see this message, it means the renderer has not been properly enabled
for the frontend that you are using. For more information, see
https://altair-viz.github.io/user_guide/display_frontends.html#troubleshooting
"""
renderers = RendererRegistry(entry_point_group=ENTRY_POINT_GROUP)
here = str(Path(__file__).parent)
def mimetype_renderer(spec: dict, **metadata) -> DefaultRendererReturnType:
return default_renderer_base(spec, VEGALITE_MIME_TYPE, DEFAULT_DISPLAY, **metadata)
def json_renderer(spec: dict, **metadata) -> DefaultRendererReturnType:
return json_renderer_base(spec, DEFAULT_DISPLAY, **metadata)
def png_renderer(spec: dict, **metadata) -> dict[str, bytes]:
# To get proper return value type, would need to write complex
# overload signatures for spec_to_mimebundle based on `format`
return spec_to_mimebundle( # type: ignore[return-value]
spec,
format="png",
mode="vega-lite",
vega_version=VEGA_VERSION,
vegaembed_version=VEGAEMBED_VERSION,
vegalite_version=VEGALITE_VERSION,
**metadata,
)
def svg_renderer(spec: dict, **metadata) -> dict[str, str]:
# To get proper return value type, would need to write complex
# overload signatures for spec_to_mimebundle based on `format`
return spec_to_mimebundle(
spec,
format="svg",
mode="vega-lite",
vega_version=VEGA_VERSION,
vegaembed_version=VEGAEMBED_VERSION,
vegalite_version=VEGALITE_VERSION,
**metadata,
)
def jupyter_renderer(spec: dict, **metadata):
"""Render chart using the JupyterChart Jupyter Widget."""
from altair import Chart, JupyterChart
# Configure offline mode
offline = metadata.get("offline", False)
# mypy doesn't see the enable_offline class method for some reason
JupyterChart.enable_offline(offline=offline) # type: ignore[attr-defined]
# propagate embed options
embed_options = metadata.get("embed_options")
# Need to ignore attr-defined mypy rule because mypy doesn't see _repr_mimebundle_
# conditionally defined in AnyWidget
return JupyterChart(
chart=Chart.from_dict(spec), embed_options=embed_options
)._repr_mimebundle_() # type: ignore[attr-defined]
def browser_renderer(
spec: dict, offline=False, using=None, port=0, **metadata
) -> dict[str, str]:
from altair.utils._show import open_html_in_browser
if offline:
metadata["template"] = "inline"
mimebundle = spec_to_mimebundle(
spec,
format="html",
mode="vega-lite",
vega_version=VEGA_VERSION,
vegaembed_version=VEGAEMBED_VERSION,
vegalite_version=VEGALITE_VERSION,
**metadata,
)
html = mimebundle["text/html"]
open_html_in_browser(html, using=using, port=port)
return {}
html_renderer = HTMLRenderer(
mode="vega-lite",
template="universal",
vega_version=VEGA_VERSION,
vegaembed_version=VEGAEMBED_VERSION,
vegalite_version=VEGALITE_VERSION,
)
olli_renderer = HTMLRenderer(
mode="vega-lite",
template="olli",
vega_version=VEGA_VERSION,
vegaembed_version=VEGAEMBED_VERSION,
vegalite_version=VEGALITE_VERSION,
)
renderers.register("default", html_renderer)
renderers.register("html", html_renderer)
renderers.register("colab", html_renderer)
renderers.register("kaggle", html_renderer)
renderers.register("zeppelin", html_renderer)
renderers.register("mimetype", mimetype_renderer)
renderers.register("jupyterlab", mimetype_renderer)
renderers.register("nteract", mimetype_renderer)
renderers.register("json", json_renderer)
renderers.register("png", png_renderer)
renderers.register("svg", svg_renderer)
# FIXME: Caused by upstream # type: ignore[unreachable]
# https://github.com/manzt/anywidget/blob/b7961305a7304f4d3def1fafef0df65db56cf41e/anywidget/widget.py#L80-L81
renderers.register("jupyter", jupyter_renderer) # pyright: ignore[reportArgumentType]
renderers.register("browser", browser_renderer)
renderers.register("olli", olli_renderer)
renderers.enable("default")
class VegaLite(Displayable):
"""An IPython/Jupyter display class for rendering VegaLite 5."""
renderers = renderers
schema_path = (__name__, "schema/vega-lite-schema.json")
def vegalite(spec: dict, validate: bool = True) -> None:
"""
Render and optionally validate a VegaLite 5 spec.
This will use the currently enabled renderer to render the spec.
Parameters
----------
spec: dict
A fully compliant VegaLite 5 spec, with the data portion fully processed.
validate: bool
Should the spec be validated against the VegaLite 5 schema?
"""
from IPython.display import display
display(VegaLite(spec, validate=validate))

View File

@@ -0,0 +1,571 @@
# ruff: noqa: F403, F405
# The contents of this file are automatically written by
# tools/generate_schema_wrapper.py. Do not modify directly.
from altair.vegalite.v5.schema import channels, core
from altair.vegalite.v5.schema.channels import *
from altair.vegalite.v5.schema.core import *
SCHEMA_VERSION = "v5.20.1"
SCHEMA_URL = "https://vega.github.io/schema/vega-lite/v5.20.1.json"
__all__ = [
"SCHEMA_URL",
"SCHEMA_VERSION",
"URI",
"X2",
"Y2",
"Aggregate",
"AggregateOp",
"AggregateTransform",
"AggregatedFieldDef",
"Align",
"AllSortString",
"Angle",
"AngleDatum",
"AngleValue",
"AnyMark",
"AnyMarkConfig",
"AreaConfig",
"ArgmaxDef",
"ArgminDef",
"AutoSizeParams",
"AutosizeType",
"Axis",
"AxisConfig",
"AxisOrient",
"AxisResolveMap",
"BBox",
"BarConfig",
"BaseTitleNoValueRefs",
"Baseline",
"BinExtent",
"BinParams",
"BinTransform",
"BindCheckbox",
"BindDirect",
"BindInput",
"BindRadioSelect",
"BindRange",
"Binding",
"BinnedTimeUnit",
"Blend",
"BoxPlot",
"BoxPlotConfig",
"BoxPlotDef",
"BrushConfig",
"CalculateTransform",
"Categorical",
"Color",
"ColorDatum",
"ColorDef",
"ColorName",
"ColorScheme",
"ColorValue",
"Column",
"CompositeMark",
"CompositeMarkDef",
"CompositionConfig",
"ConcatSpecGenericSpec",
"ConditionalAxisColor",
"ConditionalAxisLabelAlign",
"ConditionalAxisLabelBaseline",
"ConditionalAxisLabelFontStyle",
"ConditionalAxisLabelFontWeight",
"ConditionalAxisNumber",
"ConditionalAxisNumberArray",
"ConditionalAxisPropertyAlignnull",
"ConditionalAxisPropertyColornull",
"ConditionalAxisPropertyFontStylenull",
"ConditionalAxisPropertyFontWeightnull",
"ConditionalAxisPropertyTextBaselinenull",
"ConditionalAxisPropertynumberArraynull",
"ConditionalAxisPropertynumbernull",
"ConditionalAxisPropertystringnull",
"ConditionalAxisString",
"ConditionalMarkPropFieldOrDatumDef",
"ConditionalMarkPropFieldOrDatumDefTypeForShape",
"ConditionalParameterMarkPropFieldOrDatumDef",
"ConditionalParameterMarkPropFieldOrDatumDefTypeForShape",
"ConditionalParameterStringFieldDef",
"ConditionalParameterValueDefGradientstringnullExprRef",
"ConditionalParameterValueDefTextExprRef",
"ConditionalParameterValueDefnumber",
"ConditionalParameterValueDefnumberArrayExprRef",
"ConditionalParameterValueDefnumberExprRef",
"ConditionalParameterValueDefstringExprRef",
"ConditionalParameterValueDefstringnullExprRef",
"ConditionalPredicateMarkPropFieldOrDatumDef",
"ConditionalPredicateMarkPropFieldOrDatumDefTypeForShape",
"ConditionalPredicateStringFieldDef",
"ConditionalPredicateValueDefAlignnullExprRef",
"ConditionalPredicateValueDefColornullExprRef",
"ConditionalPredicateValueDefFontStylenullExprRef",
"ConditionalPredicateValueDefFontWeightnullExprRef",
"ConditionalPredicateValueDefGradientstringnullExprRef",
"ConditionalPredicateValueDefTextBaselinenullExprRef",
"ConditionalPredicateValueDefTextExprRef",
"ConditionalPredicateValueDefnumber",
"ConditionalPredicateValueDefnumberArrayExprRef",
"ConditionalPredicateValueDefnumberArraynullExprRef",
"ConditionalPredicateValueDefnumberExprRef",
"ConditionalPredicateValueDefnumbernullExprRef",
"ConditionalPredicateValueDefstringExprRef",
"ConditionalPredicateValueDefstringnullExprRef",
"ConditionalStringFieldDef",
"ConditionalValueDefGradientstringnullExprRef",
"ConditionalValueDefTextExprRef",
"ConditionalValueDefnumber",
"ConditionalValueDefnumberArrayExprRef",
"ConditionalValueDefnumberExprRef",
"ConditionalValueDefstringExprRef",
"ConditionalValueDefstringnullExprRef",
"Config",
"CsvDataFormat",
"Cursor",
"Cyclical",
"Data",
"DataFormat",
"DataSource",
"Datasets",
"DateTime",
"DatumChannelMixin",
"DatumDef",
"Day",
"DensityTransform",
"DerivedStream",
"Description",
"DescriptionValue",
"Detail",
"DictInlineDataset",
"DictSelectionInit",
"DictSelectionInitInterval",
"Diverging",
"DomainUnionWith",
"DsvDataFormat",
"Element",
"Encoding",
"EncodingSortField",
"ErrorBand",
"ErrorBandConfig",
"ErrorBandDef",
"ErrorBar",
"ErrorBarConfig",
"ErrorBarDef",
"ErrorBarExtent",
"EventStream",
"EventType",
"Expr",
"ExprRef",
"ExtentTransform",
"Facet",
"FacetEncodingFieldDef",
"FacetFieldDef",
"FacetSpec",
"FacetedEncoding",
"FacetedUnitSpec",
"Feature",
"FeatureCollection",
"FeatureGeometryGeoJsonProperties",
"Field",
"FieldChannelMixin",
"FieldDefWithoutScale",
"FieldEqualPredicate",
"FieldGTEPredicate",
"FieldGTPredicate",
"FieldLTEPredicate",
"FieldLTPredicate",
"FieldName",
"FieldOneOfPredicate",
"FieldOrDatumDefWithConditionDatumDefGradientstringnull",
"FieldOrDatumDefWithConditionDatumDefnumber",
"FieldOrDatumDefWithConditionDatumDefnumberArray",
"FieldOrDatumDefWithConditionDatumDefstringnull",
"FieldOrDatumDefWithConditionMarkPropFieldDefGradientstringnull",
"FieldOrDatumDefWithConditionMarkPropFieldDefTypeForShapestringnull",
"FieldOrDatumDefWithConditionMarkPropFieldDefnumber",
"FieldOrDatumDefWithConditionMarkPropFieldDefnumberArray",
"FieldOrDatumDefWithConditionStringDatumDefText",
"FieldOrDatumDefWithConditionStringFieldDefText",
"FieldOrDatumDefWithConditionStringFieldDefstring",
"FieldRange",
"FieldRangePredicate",
"FieldValidPredicate",
"Fill",
"FillDatum",
"FillOpacity",
"FillOpacityDatum",
"FillOpacityValue",
"FillValue",
"FilterTransform",
"Fit",
"FlattenTransform",
"FoldTransform",
"FontStyle",
"FontWeight",
"FormatConfig",
"Generator",
"GenericUnitSpecEncodingAnyMark",
"GeoJsonFeature",
"GeoJsonFeatureCollection",
"GeoJsonProperties",
"Geometry",
"GeometryCollection",
"Gradient",
"GradientStop",
"GraticuleGenerator",
"GraticuleParams",
"HConcatSpecGenericSpec",
"Header",
"HeaderConfig",
"HexColor",
"Href",
"HrefValue",
"ImputeMethod",
"ImputeParams",
"ImputeSequence",
"ImputeTransform",
"InlineData",
"InlineDataset",
"Interpolate",
"IntervalSelectionConfig",
"IntervalSelectionConfigWithoutType",
"JoinAggregateFieldDef",
"JoinAggregateTransform",
"JsonDataFormat",
"Key",
"LabelOverlap",
"LatLongDef",
"LatLongFieldDef",
"Latitude",
"Latitude2",
"Latitude2Datum",
"Latitude2Value",
"LatitudeDatum",
"LayerRepeatMapping",
"LayerRepeatSpec",
"LayerSpec",
"LayoutAlign",
"Legend",
"LegendBinding",
"LegendConfig",
"LegendOrient",
"LegendResolveMap",
"LegendStreamBinding",
"LineConfig",
"LineString",
"LinearGradient",
"LocalMultiTimeUnit",
"LocalSingleTimeUnit",
"Locale",
"LoessTransform",
"LogicalAndPredicate",
"LogicalNotPredicate",
"LogicalOrPredicate",
"Longitude",
"Longitude2",
"Longitude2Datum",
"Longitude2Value",
"LongitudeDatum",
"LookupSelection",
"LookupTransform",
"Mark",
"MarkConfig",
"MarkDef",
"MarkInvalidDataMode",
"MarkPropDefGradientstringnull",
"MarkPropDefnumber",
"MarkPropDefnumberArray",
"MarkPropDefstringnullTypeForShape",
"MarkType",
"MergedStream",
"Month",
"MultiLineString",
"MultiPoint",
"MultiPolygon",
"MultiTimeUnit",
"NamedData",
"NonArgAggregateOp",
"NonLayerRepeatSpec",
"NonNormalizedSpec",
"NumberLocale",
"NumericArrayMarkPropDef",
"NumericMarkPropDef",
"OffsetDef",
"Opacity",
"OpacityDatum",
"OpacityValue",
"Order",
"OrderFieldDef",
"OrderOnlyDef",
"OrderValue",
"OrderValueDef",
"Orient",
"Orientation",
"OverlayMarkDef",
"Padding",
"ParameterExtent",
"ParameterName",
"ParameterPredicate",
"Parse",
"ParseValue",
"PivotTransform",
"Point",
"PointSelectionConfig",
"PointSelectionConfigWithoutType",
"PolarDef",
"Polygon",
"Position",
"Position2Def",
"PositionDatumDef",
"PositionDatumDefBase",
"PositionDef",
"PositionFieldDef",
"PositionFieldDefBase",
"PositionValueDef",
"Predicate",
"PredicateComposition",
"PrimitiveValue",
"Projection",
"ProjectionConfig",
"ProjectionType",
"QuantileTransform",
"RadialGradient",
"Radius",
"Radius2",
"Radius2Datum",
"Radius2Value",
"RadiusDatum",
"RadiusValue",
"RangeConfig",
"RangeEnum",
"RangeRaw",
"RangeRawArray",
"RangeScheme",
"RectConfig",
"RegressionTransform",
"RelativeBandSize",
"RepeatMapping",
"RepeatRef",
"RepeatSpec",
"Resolve",
"ResolveMode",
"Root",
"Row",
"RowColLayoutAlign",
"RowColboolean",
"RowColnumber",
"RowColumnEncodingFieldDef",
"SampleTransform",
"Scale",
"ScaleBinParams",
"ScaleBins",
"ScaleConfig",
"ScaleDatumDef",
"ScaleFieldDef",
"ScaleInterpolateEnum",
"ScaleInterpolateParams",
"ScaleInvalidDataConfig",
"ScaleInvalidDataShowAsValueangle",
"ScaleInvalidDataShowAsValuecolor",
"ScaleInvalidDataShowAsValuefill",
"ScaleInvalidDataShowAsValuefillOpacity",
"ScaleInvalidDataShowAsValueopacity",
"ScaleInvalidDataShowAsValueradius",
"ScaleInvalidDataShowAsValueshape",
"ScaleInvalidDataShowAsValuesize",
"ScaleInvalidDataShowAsValuestroke",
"ScaleInvalidDataShowAsValuestrokeDash",
"ScaleInvalidDataShowAsValuestrokeOpacity",
"ScaleInvalidDataShowAsValuestrokeWidth",
"ScaleInvalidDataShowAsValuetheta",
"ScaleInvalidDataShowAsValuex",
"ScaleInvalidDataShowAsValuexOffset",
"ScaleInvalidDataShowAsValuey",
"ScaleInvalidDataShowAsValueyOffset",
"ScaleInvalidDataShowAsangle",
"ScaleInvalidDataShowAscolor",
"ScaleInvalidDataShowAsfill",
"ScaleInvalidDataShowAsfillOpacity",
"ScaleInvalidDataShowAsopacity",
"ScaleInvalidDataShowAsradius",
"ScaleInvalidDataShowAsshape",
"ScaleInvalidDataShowAssize",
"ScaleInvalidDataShowAsstroke",
"ScaleInvalidDataShowAsstrokeDash",
"ScaleInvalidDataShowAsstrokeOpacity",
"ScaleInvalidDataShowAsstrokeWidth",
"ScaleInvalidDataShowAstheta",
"ScaleInvalidDataShowAsx",
"ScaleInvalidDataShowAsxOffset",
"ScaleInvalidDataShowAsy",
"ScaleInvalidDataShowAsyOffset",
"ScaleResolveMap",
"ScaleType",
"SchemaBase",
"SchemeParams",
"SecondaryFieldDef",
"SelectionConfig",
"SelectionInit",
"SelectionInitInterval",
"SelectionInitIntervalMapping",
"SelectionInitMapping",
"SelectionParameter",
"SelectionResolution",
"SelectionType",
"SequenceGenerator",
"SequenceParams",
"SequentialMultiHue",
"SequentialSingleHue",
"Shape",
"ShapeDatum",
"ShapeDef",
"ShapeValue",
"SharedEncoding",
"SingleDefUnitChannel",
"SingleTimeUnit",
"Size",
"SizeDatum",
"SizeValue",
"Sort",
"SortArray",
"SortByChannel",
"SortByChannelDesc",
"SortByEncoding",
"SortField",
"SortOrder",
"Spec",
"SphereGenerator",
"StackOffset",
"StackTransform",
"StandardType",
"Step",
"StepFor",
"Stream",
"StringFieldDef",
"StringFieldDefWithCondition",
"StringValueDefWithCondition",
"Stroke",
"StrokeCap",
"StrokeDash",
"StrokeDashDatum",
"StrokeDashValue",
"StrokeDatum",
"StrokeJoin",
"StrokeOpacity",
"StrokeOpacityDatum",
"StrokeOpacityValue",
"StrokeValue",
"StrokeWidth",
"StrokeWidthDatum",
"StrokeWidthValue",
"StyleConfigIndex",
"SymbolShape",
"Text",
"TextBaseline",
"TextDatum",
"TextDef",
"TextDirection",
"TextValue",
"Theta",
"Theta2",
"Theta2Datum",
"Theta2Value",
"ThetaDatum",
"ThetaValue",
"TickConfig",
"TickCount",
"TimeInterval",
"TimeIntervalStep",
"TimeLocale",
"TimeUnit",
"TimeUnitParams",
"TimeUnitTransform",
"TimeUnitTransformParams",
"TitleAnchor",
"TitleConfig",
"TitleFrame",
"TitleOrient",
"TitleParams",
"Tooltip",
"TooltipContent",
"TooltipValue",
"TopLevelConcatSpec",
"TopLevelFacetSpec",
"TopLevelHConcatSpec",
"TopLevelLayerSpec",
"TopLevelParameter",
"TopLevelRepeatSpec",
"TopLevelSelectionParameter",
"TopLevelSpec",
"TopLevelUnitSpec",
"TopLevelVConcatSpec",
"TopoDataFormat",
"Transform",
"Type",
"TypeForShape",
"TypedFieldDef",
"UnitSpec",
"UnitSpecWithFrame",
"Url",
"UrlData",
"UrlValue",
"UtcMultiTimeUnit",
"UtcSingleTimeUnit",
"VConcatSpecGenericSpec",
"ValueChannelMixin",
"ValueDefWithConditionMarkPropFieldOrDatumDefGradientstringnull",
"ValueDefWithConditionMarkPropFieldOrDatumDefTypeForShapestringnull",
"ValueDefWithConditionMarkPropFieldOrDatumDefnumber",
"ValueDefWithConditionMarkPropFieldOrDatumDefnumberArray",
"ValueDefWithConditionMarkPropFieldOrDatumDefstringnull",
"ValueDefWithConditionStringFieldDefText",
"ValueDefnumber",
"ValueDefnumberwidthheightExprRef",
"VariableParameter",
"Vector2DateTime",
"Vector2Vector2number",
"Vector2boolean",
"Vector2number",
"Vector2string",
"Vector3number",
"Vector7string",
"Vector10string",
"Vector12string",
"VegaLiteSchema",
"ViewBackground",
"ViewConfig",
"WindowEventType",
"WindowFieldDef",
"WindowOnlyOp",
"WindowTransform",
"X",
"X2Datum",
"X2Value",
"XDatum",
"XError",
"XError2",
"XError2Value",
"XErrorValue",
"XOffset",
"XOffsetDatum",
"XOffsetValue",
"XValue",
"Y",
"Y2Datum",
"Y2Value",
"YDatum",
"YError",
"YError2",
"YError2Value",
"YErrorValue",
"YOffset",
"YOffsetDatum",
"YOffsetValue",
"YValue",
"channels",
"core",
"load_schema",
"with_property_setters",
]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,125 @@
"""Tools for enabling and registering chart themes."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Final, Literal, get_args
from altair.utils.deprecation import deprecated_static_only
from altair.utils.plugin_registry import Plugin, PluginRegistry
from altair.vegalite.v5.schema._config import ThemeConfig
from altair.vegalite.v5.schema._typing import VegaThemes
if TYPE_CHECKING:
import sys
from functools import partial
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 TypeAlias
else:
from typing_extensions import TypeAlias
from altair.utils.plugin_registry import PluginEnabler
AltairThemes: TypeAlias = Literal["default", "opaque"]
VEGA_THEMES: list[LiteralString] = list(get_args(VegaThemes))
# HACK: See for `LiteralString` requirement in `name`
# https://github.com/vega/altair/pull/3526#discussion_r1743350127
class ThemeRegistry(PluginRegistry[Plugin[ThemeConfig], ThemeConfig]):
def enable(
self,
name: LiteralString | AltairThemes | VegaThemes | None = None,
**options: Any,
) -> PluginEnabler[Plugin[ThemeConfig], ThemeConfig]:
"""
Enable a theme by name.
This can be either called directly, or used as a context manager.
Parameters
----------
name : string (optional)
The name of the theme to enable. If not specified, then use the
current active name.
**options :
Any additional parameters will be passed to the theme as keyword
arguments
Returns
-------
PluginEnabler:
An object that allows enable() to be used as a context manager
Notes
-----
Default `vega` themes can be previewed at https://vega.github.io/vega-themes/
"""
return super().enable(name, **options)
def get(self) -> partial[ThemeConfig] | Plugin[ThemeConfig] | None:
"""Return the currently active theme."""
return super().get()
def names(self) -> list[str]:
"""Return the names of the registered and entry points themes."""
return super().names()
@deprecated_static_only(
"Deprecated since `altair=5.5.0`. Use @altair.theme.register instead.",
category=None,
)
def register(
self, name: str, value: Plugin[ThemeConfig] | None
) -> Plugin[ThemeConfig] | None:
return super().register(name, value)
class VegaTheme:
"""Implementation of a builtin vega theme."""
def __init__(self, theme: str) -> None:
self.theme = theme
def __call__(self) -> ThemeConfig:
return {
"usermeta": {"embedOptions": {"theme": self.theme}},
"config": {"view": {"continuousWidth": 300, "continuousHeight": 300}},
}
def __repr__(self) -> str:
return f"VegaTheme({self.theme!r})"
# The entry point group that can be used by other packages to declare other
# themes that will be auto-detected. Explicit registration is also
# allowed by the PluginRegistry API.
ENTRY_POINT_GROUP: Final = "altair.vegalite.v5.theme"
# NOTE: `themes` def has an entry point group
themes = ThemeRegistry(entry_point_group=ENTRY_POINT_GROUP)
themes.register(
"default",
lambda: {"config": {"view": {"continuousWidth": 300, "continuousHeight": 300}}},
)
themes.register(
"opaque",
lambda: {
"config": {
"background": "white",
"view": {"continuousWidth": 300, "continuousHeight": 300},
}
},
)
themes.register("none", ThemeConfig)
for theme in VEGA_THEMES:
themes.register(theme, VegaTheme(theme))
themes.enable("default")