diff --git a/altair/vegalite/v5/api.py b/altair/vegalite/v5/api.py index c861e0800..e7704d7f3 100644 --- a/altair/vegalite/v5/api.py +++ b/altair/vegalite/v5/api.py @@ -112,6 +112,14 @@ GetAttrExpression, GetItemExpression, ) + from .schema._typing import ( + ImputeMethod_T, + SelectionType_T, + SelectionResolution_T, + SingleDefUnitChannel_T, + StackOffset_T, + ) + ChartDataType: TypeAlias = Optional[Union[DataType, core.Data, str, core.Generator]] _TSchemaBase = TypeVar("_TSchemaBase", bound=core.SchemaBase) @@ -1251,9 +1259,7 @@ def param( return parameter -def _selection( - type: Optional[Literal["interval", "point"]] = Undefined, **kwds -) -> Parameter: +def _selection(type: Optional[SelectionType_T] = Undefined, **kwds) -> Parameter: # We separate out the parameter keywords from the selection keywords select_kwds = {"name", "bind", "value", "empty", "init", "views"} @@ -1283,9 +1289,7 @@ def _selection( message="""'selection' is deprecated. Use 'selection_point()' or 'selection_interval()' instead; these functions also include more helpful docstrings.""" ) -def selection( - type: Optional[Literal["interval", "point"]] = Undefined, **kwds -) -> Parameter: +def selection(type: Optional[SelectionType_T] = Undefined, **kwds) -> Parameter: """ Users are recommended to use either 'selection_point' or 'selection_interval' instead, depending on the type of parameter they want to create. @@ -1314,10 +1318,10 @@ def selection_interval( bind: Optional[Binding | str] = Undefined, empty: Optional[bool] = Undefined, expr: Optional[str | Expr | Expression] = Undefined, - encodings: Optional[list[str]] = Undefined, + encodings: Optional[list[SingleDefUnitChannel_T]] = Undefined, on: Optional[str] = Undefined, clear: Optional[str | bool] = Undefined, - resolve: Optional[Literal["global", "union", "intersect"]] = Undefined, + resolve: Optional[SelectionResolution_T] = Undefined, mark: Optional[Mark] = Undefined, translate: Optional[str | bool] = Undefined, zoom: Optional[str | bool] = Undefined, @@ -1426,11 +1430,11 @@ def selection_point( bind: Optional[Binding | str] = Undefined, empty: Optional[bool] = Undefined, expr: Optional[Expr] = Undefined, - encodings: Optional[list[str]] = Undefined, + encodings: Optional[list[SingleDefUnitChannel_T]] = Undefined, fields: Optional[list[str]] = Undefined, on: Optional[str] = Undefined, clear: Optional[str | bool] = Undefined, - resolve: Optional[Literal["global", "union", "intersect"]] = Undefined, + resolve: Optional[SelectionResolution_T] = Undefined, toggle: Optional[str | bool] = Undefined, nearest: Optional[bool] = Undefined, **kwds, @@ -2549,9 +2553,7 @@ def transform_impute( frame: Optional[list[int | None]] = Undefined, groupby: Optional[list[str | FieldName]] = Undefined, keyvals: Optional[list[Any] | ImputeSequence] = Undefined, - method: Optional[ - Literal["value", "mean", "median", "max", "min"] | ImputeMethod - ] = Undefined, + method: Optional[ImputeMethod_T | ImputeMethod] = Undefined, value=Undefined, ) -> Self: """ @@ -3074,7 +3076,7 @@ def transform_stack( as_: str | FieldName | list[str], stack: str | FieldName, groupby: list[str | FieldName], - offset: Optional[Literal["zero", "center", "normalize"]] = Undefined, + offset: Optional[StackOffset_T] = Undefined, sort: Optional[list[SortField]] = Undefined, ) -> Self: """ @@ -3757,7 +3759,7 @@ def interactive( copy of self, with interactive axes added """ - encodings = [] + encodings: list[SingleDefUnitChannel_T] = [] if bind_x: encodings.append("x") if bind_y: @@ -4047,7 +4049,7 @@ def interactive( copy of self, with interactive axes added """ - encodings = [] + encodings: list[SingleDefUnitChannel_T] = [] if bind_x: encodings.append("x") if bind_y: @@ -4144,7 +4146,7 @@ def interactive( copy of self, with interactive axes added """ - encodings = [] + encodings: list[SingleDefUnitChannel_T] = [] if bind_x: encodings.append("x") if bind_y: @@ -4243,7 +4245,7 @@ def interactive( copy of self, with interactive axes added """ - encodings = [] + encodings: list[SingleDefUnitChannel_T] = [] if bind_x: encodings.append("x") if bind_y: diff --git a/doc/user_guide/encodings/channels.rst b/doc/user_guide/encodings/channels.rst index e0e00cb29..7feb9136e 100644 --- a/doc/user_guide/encodings/channels.rst +++ b/doc/user_guide/encodings/channels.rst @@ -41,7 +41,7 @@ Channel Altair Class Description Example angle :class:`Angle` The angle of the mark :ref:`gallery_wind_vector_map` color :class:`Color` The color of the mark :ref:`gallery_simple_heatmap` fill :class:`Fill` The fill for the mark :ref:`gallery_ridgeline_plot` -fillopacity :class:`FillOpacity` The opacity of the mark's fill N/A +fillOpacity :class:`FillOpacity` The opacity of the mark's fill N/A opacity :class:`Opacity` The opacity of the mark :ref:`gallery_horizon_graph` radius :class:`Radius` The radius or the mark :ref:`gallery_radial_chart` shape :class:`Shape` The shape of the mark :ref:`gallery_us_incomebrackets_by_state_facet` diff --git a/tests/examples_arguments_syntax/scatter_point_paths_hover.py b/tests/examples_arguments_syntax/scatter_point_paths_hover.py new file mode 100644 index 000000000..ee755b9e0 --- /dev/null +++ b/tests/examples_arguments_syntax/scatter_point_paths_hover.py @@ -0,0 +1,148 @@ +""" +Scatter plot with point paths on hover with search box +====================================================== +This example combines cross-sectional analysis (comparing countries at a single point in time) +with longitudinal analysis (tracking changes in individual countries over time), using +an interactive visualization technique inspired by [this Vega example](https://vega.github.io/vega/examples/global-development/). + +Key features: +1. Point Paths. On hover, shows data trajectories using a trail mark that +thickens from past to present, clearly indicating the direction of time. +2. Search Box. Implements a case-insensitive regex filter for country names, +enabling dynamic, flexible data point selection to enhance exploratory analysis. +""" +# category: interactive charts +import altair as alt +from vega_datasets import data + +# Data source +source = data.gapminder.url + +# X-value slider +x_slider = alt.binding_range(min=1955, max=2005, step=5, name='Year ') +x_select = alt.selection_point(name="x_select", fields=['year'], bind=x_slider, value=1980) + +# Hover selection +hover = alt.selection_point(on='mouseover', fields=['country'], empty=False) +# A separate hover for the points since these need empty=True +hover_point_opacity = alt.selection_point(on='mouseover', fields=['country']) + +# Search box for country name +search_box = alt.param( + value='', + bind=alt.binding(input='search', placeholder="Country", name='Search ') +) + +# Base chart +base = alt.Chart(source).encode( + x=alt.X('fertility:Q', scale=alt.Scale(zero=False), title='Babies per woman (total fertility rate)'), + y=alt.Y('life_expect:Q', scale=alt.Scale(zero=False), title='Life expectancy'), + color=alt.Color('region:N', title='Region', legend=alt.Legend(orient='bottom-left', titleFontSize=14, labelFontSize=12), scale=alt.Scale(scheme='dark2')), + detail='country:N' +).transform_calculate( + region="""{ + '0': 'South Asia', + '1': 'Europe & Central Asia', + '2': 'Sub-Saharan Africa', + '3': 'The Americas', + '4': 'East Asia & Pacific', + '5': 'Middle East & North Africa' + }[datum.cluster]""" +).transform_filter( + # Exclude North Korea and South Korea due to source data error + "datum.country !== 'North Korea' && datum.country !== 'South Korea'" +) + +# Points that are always visible (filtered by slider and search) +visible_points = base.mark_circle(size=100).encode( + opacity=alt.condition( + hover_point_opacity + & alt.expr.test(alt.expr.regexp(search_box, 'i'), alt.datum.country), + alt.value(0.8), + alt.value(0.1) + ) + ).transform_filter( + x_select + ).add_params( + hover, + hover_point_opacity, + x_select +) + +hover_line = alt.layer( + # Line layer + base.mark_trail().encode( + order=alt.Order( + 'year:Q', + sort='ascending' + ), + size=alt.Size( + 'year:Q', + scale=alt.Scale(domain=[1955, 2005], range=[1, 12]), + legend=None + ), + opacity=alt.condition(hover, alt.value(0.3), alt.value(0)), + color=alt.value('#222222') + ), + # Point layer + base.mark_point(size=50).encode( + opacity=alt.condition(hover, alt.value(0.8), alt.value(0)), + ) +) + +# Year labels +year_labels = base.mark_text(align='left', dx=5, dy=-5, fontSize=14).encode( + text='year:O', + color=alt.value('#222222') +).transform_filter(hover) + +# Country labels +country_labels = alt.Chart(source).mark_text( + align='left', + dx=-15, + dy=-25, + fontSize=18, + fontWeight='bold' +).encode( + x='fertility:Q', + y='life_expect:Q', + text='country:N', + color=alt.value('black'), + opacity=alt.condition(hover, alt.value(1), alt.value(0)) +).transform_window( + rank='rank(life_expect)', + sort=[alt.SortField('life_expect', order='descending')], + groupby=['country'] # places label atop highest point on y-axis on hover +).transform_filter( + alt.datum.rank == 1 +).transform_aggregate( + life_expect='max(life_expect)', + fertility='max(fertility)', + groupby=['country'] +) + +background_year = alt.Chart(source).mark_text( + baseline='middle', + fontSize=96, + opacity=0.2 +).encode( + text='year:O' +).transform_filter( + x_select +).transform_aggregate( + year='max(year)' +) + +# Combine all layers +chart = alt.layer( + visible_points, year_labels, country_labels, hover_line, background_year +).properties( + width=500, + height=500, + padding=10 # Padding ensures labels fit +).configure_axis( + labelFontSize=12, + titleFontSize=12 +).add_params(search_box) + +chart \ No newline at end of file diff --git a/tests/examples_methods_syntax/scatter_point_paths_hover.py b/tests/examples_methods_syntax/scatter_point_paths_hover.py new file mode 100644 index 000000000..a6cc747c7 --- /dev/null +++ b/tests/examples_methods_syntax/scatter_point_paths_hover.py @@ -0,0 +1,141 @@ +""" +Scatter plot with point paths on hover with search box +====================================================== +This example combines cross-sectional analysis (comparing countries at a single point in time) +with longitudinal analysis (tracking changes in individual countries over time), using +an interactive visualization technique inspired by [this Vega example](https://vega.github.io/vega/examples/global-development/) + +Key features: +1. Point Paths. On hover, shows data trajectories using a trail mark that +thickens from past to present, clearly indicating the direction of time. +2. Search Box. Implements a case-insensitive regex filter for country names, +enabling dynamic, flexible data point selection to enhance exploratory analysis. +""" +# category: interactive charts +import altair as alt +from vega_datasets import data + +# Data source +source = data.gapminder.url + +# X-value slider +x_slider = alt.binding_range(min=1955, max=2005, step=5, name='Year ') +x_select = alt.selection_point(name="x_select", fields=['year'], bind=x_slider, value=1980) + +# Hover selection +hover = alt.selection_point(on='mouseover', fields=['country'], empty=False) +# A separate hover for the points since these need empty=True +hover_point_opacity = alt.selection_point(on='mouseover', fields=['country']) + +# Search box for country name +search_box = alt.param( + value='', + bind=alt.binding(input='search', placeholder="Country", name='Search ') +) + +# Base chart +base = alt.Chart(source).encode( + alt.X('fertility:Q').scale(zero=False).title('Babies per woman (total fertility rate)'), + alt.Y('life_expect:Q').scale(zero=False).title('Life expectancy'), + alt.Color('region:N').scale(scheme='dark2').legend(orient='bottom-left', titleFontSize=14, labelFontSize=12).title('Region'), + alt.Detail('country:N') +).transform_calculate( + region="""{ + '0': 'South Asia', + '1': 'Europe & Central Asia', + '2': 'Sub-Saharan Africa', + '3': 'The Americas', + '4': 'East Asia & Pacific', + '5': 'Middle East & North Africa' + }[datum.cluster]""" +).transform_filter( + # Exclude North Korea and South Korea due to source data error + "datum.country !== 'North Korea' && datum.country !== 'South Korea'" +) + +# Points that are always visible (filtered by slider and search) +visible_points = base.mark_circle(size=100).encode( + opacity=alt.condition( + hover_point_opacity + & alt.expr.test(alt.expr.regexp(search_box, 'i'), alt.datum.country), + alt.value(0.8), + alt.value(0.1) + ) + ).transform_filter( + x_select + ).add_params( + hover, + hover_point_opacity, + x_select +) + +hover_line = alt.layer( + # Line layer + base.mark_trail().encode( + alt.Order('year:Q').sort('ascending'), + alt.Size('year:Q').scale(domain=[1955, 2005], range=[1, 12]).legend(None), + opacity=alt.condition(hover, alt.value(0.3), alt.value(0)), + color=alt.value('#222222') + ), + # Point layer + base.mark_point(size=50).encode( + opacity=alt.condition(hover, alt.value(0.8), alt.value(0)), + ) +) + +# Year labels +year_labels = base.mark_text(align='left', dx=5, dy=-5, fontSize=14).encode( + text='year:O', + color=alt.value('#222222') +).transform_filter(hover) + +# Country labels +country_labels = alt.Chart(source).mark_text( + align='left', + dx=-15, + dy=-25, + fontSize=18, + fontWeight='bold' +).encode( + x='fertility:Q', + y='life_expect:Q', + text='country:N', + color=alt.value('black'), + opacity=alt.condition(hover, alt.value(1), alt.value(0)) +).transform_window( + rank='rank(life_expect)', + sort=[alt.SortField('life_expect', order='descending')], + groupby=['country'] # places label atop highest point on y-axis on hover +).transform_filter( + alt.datum.rank == 1 +).transform_aggregate( + life_expect='max(life_expect)', + fertility='max(fertility)', + groupby=['country'] +) + +background_year = alt.Chart(source).mark_text( + baseline='middle', + fontSize=96, + opacity=0.2 +).encode( + text='year:O' +).transform_filter( + x_select +).transform_aggregate( + year='max(year)' +) + +# Combine all layers +chart = alt.layer( + visible_points, year_labels, country_labels, hover_line, background_year +).properties( + width=500, + height=500, + padding=10 # Padding ensures labels fit +).configure_axis( + labelFontSize=12, + titleFontSize=12 +).add_params(search_box) + +chart \ No newline at end of file