ways_py.ways module
from functools import wraps from typing import Any, Callable, cast, TypeVar import altair as alt # type: ignore from IPython.display import display # type: ignore from ipywidgets import Box, Layout, widgets # type: ignore import pandas as pd # type: ignore import traitlets # type: ignore def is_defined(v: Any) -> bool: """Altair's notion of an undefined schema property.""" return type(v).__name__ != 'UndefinedType' class AltairColorViz: """Meta-visualisation for alt.Color object. Has two components: a plot of the underlying distribution of the chart, as a vertical histogram, computed by `density_chart`, and a plot of the colours used, which serves as the y-axis "labels", computed by `used_colours`. """ @staticmethod def _field(src: alt.Chart) -> str: """Centralise the assumption that this property stores just a field name.""" return cast(str, src.encoding.color.shorthand) @staticmethod def density_chart(src: alt.Chart) -> alt.Chart: """The underlying distribution of the chart as a histogram; placed alongside 'colours used'.""" if src.encoding.color.bin: if is_defined(src.encoding.color.bin.extent): extent = src.encoding.color.bin.extent bin = alt.Bin(maxbins=100, extent=extent) y_scale = alt.Scale(domain=extent, nice=True) else: bin = alt.Bin(maxbins=100) y_scale = alt.Scale(zero=False, nice=True) else: bin = alt.Bin(maxbins=100) y_scale = alt.Scale(nice=False) ys = src.data[AltairColorViz._field(src)] # assume src.data array-like in an appropriate way y_min, y_max = min(ys), max(ys) # tickCount/tickMinStep Axis properties are ignored (perhaps because we specify bins), so hard code y_axis = alt.Y( src.encoding.color.shorthand, bin=bin, axis=alt.Axis(orient='left', grid=False, values=sorted([0, 50] + [y_min, y_max])), title=src.encoding.color.shorthand, scale=y_scale ) x_axis = alt.X( 'sum(proportion):Q', sort='descending', axis=alt.Axis(grid=False), title="density" ) # Title for both the density_chart and used_colours plots title = "Colours used" return alt.Chart(src.data, title=title) \ .transform_joinaggregate(total='count(*)') \ .transform_calculate(proportion="1 / datum.total") \ .mark_bar(color='gray') \ .encode(y_axis, x_axis) \ .properties(width=100, height=300) @staticmethod def used_colours(src: alt.Chart) -> alt.Chart: """The colours used by the chart, plotted as another (vertical) chart.""" y_axis = alt.Axis(orient='right', grid=False) x_axis = alt.Axis(labels=False, tickSize=0, grid=False, titleAngle=270, titleAlign='right') if src.encoding.color.bin: if is_defined(src.encoding.color.bin.extent): extent = src.encoding.color.bin.extent y_scale = alt.Scale(domain=extent, nice=True) else: y_scale = alt.Scale(zero=False, nice=True) chart = alt.Chart(src.data) \ .mark_rect() \ .transform_bin( as_=['y', 'y2'], bin=src.encoding.color.bin, field=AltairColorViz._field(src) ) \ .transform_calculate(x='5') \ .encode( y=alt.Y('y:Q', axis=y_axis, title="", scale=y_scale), y2='y2:Q', x=alt.X('x:Q', sort='descending', axis=x_axis, title="") ) else: y_scale = alt.Scale(nice=False) # Get a dataframe to plot where there is only one row for each unique value # of the column of the source chart data being plotted df = src.data.drop_duplicates(subset=[src.encoding.color.shorthand]) chart = alt.Chart(df) \ .mark_bar() \ .encode( y=alt.Y(src.encoding.color.shorthand, axis=y_axis, title="", scale=y_scale), x=alt.X('count()', sort='descending', axis=x_axis, title="") ) return chart.encode(src.encoding.color) \ .properties(width=20, height=300) @staticmethod def decorate(src: alt.Chart) -> alt.Chart: """Decorate a colour-ended Altair chart with meta-visualisations showing how the colours are used. Args: src: colour-encoded Altair chart to be decorated. Returns: Altair chart object: modified chart """ if not is_defined(src.encoding.color.bin): raise Exception("Can only apply decorator to chart with color.bin defined.") meta_chart: alt.Chart = (AltairColorViz.density_chart(src) | AltairColorViz.used_colours(src)) return (meta_chart | src) \ .configure_view(strokeWidth=0) \ .configure_concat(spacing=5) FuncT = TypeVar("FuncT", bound=Callable[..., Any]) """Type variable for internal module use.""" def altair_color_viz(make_chart: FuncT) -> FuncT: """Decorator which attaches an AltairColorViz meta-visualisation to a colour-encoded Altair chart. Given a function which creates an Altair chart using an alt.Color object for colour encoding, adapt that function to return the original chart decorated with an AltairColorViz meta-visualisation. """ @wraps(make_chart) def wrapper(*args: Any, **kwargs: Any) -> Any: return AltairColorViz.decorate(make_chart(*args, **kwargs)) return cast(FuncT, wrapper) class AltairColorWidgets: """WAYS widgets class for Altair color object.""" def __init__(self) -> None: """Create Jupyter widgets that can be used as input to Altair color object in a Jupyter notebook.""" # Checkbox widget that determines whether binning is enabled self.bin = widgets.RadioButtons(value='Binned', options=['Binned', 'Continuous'], description='Color Binning') # Textbox accepting integer to select the maximum number of bins self.maxbins = widgets.IntText(value=7, description='Max Bins:', continuous_update=True) # Two widgets determining where the binning of data starts and ends self.extentmin = widgets.IntText(value=0, continuous_update=True, description='Extent Min') self.extentmax = widgets.IntText(value=0, continuous_update=True, description='Extent Max') wide_Vbox = Layout(display='flex', flex_flow='column', align_items='center', width='110%') self.extent = Box(children=[self.extentmin, self.extentmax], layout=wide_Vbox) # Create a horizontal box that contains these widgets self.bin_grid = widgets.GridBox([self.bin, self.maxbins, self.extent], layout=Layout( grid_template_columns="repeat(3, 300px)") ) # list of scales from: # https://altair-viz.github.io/user_guide/generated/core/altair.ScaleType.html#altair.ScaleType scales = ['linear', 'log', 'pow', 'sqrt', 'symlog', 'identity', 'sequential', 'time', 'utc', 'quantile', 'quantize', 'threshold', 'bin-ordinal', 'ordinal', 'point', 'band'] self.scale = widgets.Dropdown(value='linear', options=scales, description='Color Scale') # list from https://vega.github.io/vega/docs/schemes/#reference schemes = ['blues', 'tealblues', 'teals', 'greens', 'browns', 'oranges', 'reds', 'purples', 'warmgreys', 'greys', 'viridis', 'magma', 'inferno', 'plasma', 'cividis', 'turbo', 'bluegreen', 'bluepurple', 'goldgreen', 'goldorange', 'goldred', 'greenblue', 'orangered', 'purplebluegreen', 'purpleblue', 'purplered', 'redpurple', 'yellowgreenblue', 'yellowgreen', 'yelloworangebrown', 'yelloworangered', 'darkblue', 'darkgold', 'darkgreen', 'darkmulti', 'darkred', 'lightgreyred', 'lightgreyteal', 'lightmulti', 'lightorange', 'lighttealblue', 'blueorange', 'brownbluegreen', 'purplegreen', 'pinkyellowgreen', 'purpleorange', 'redblue', 'redgrey', 'redyellowblue', 'redyellowgreen', 'spectral', 'rainbow', 'sinebow'] # The widgets here expose a variety of options for setting the color scheme: # colorscheme and the color range boxes are greyed out when not selected by colorschemetype self.colorschemetype = widgets.RadioButtons(value='Scheme', options=['Scheme', 'Range'], description='Color Method') self.colorscheme = widgets.Dropdown(options=schemes, description='Scheme') self.color_1 = widgets.ColorPicker(concise=True, value='red', disabled=True, description='Range') self.color_2 = widgets.ColorPicker(concise=True, value='purple', disabled=True) self.color_3 = widgets.ColorPicker(concise=True, value='blue', disabled=True) wide_Hbox = Layout(display='flex', flex_flow='row', align_items='center', width='110%') color_box = Box([self.color_1, self.color_2, self.color_3], layout=wide_Hbox) self.scale_grid = widgets.GridBox([self.colorschemetype, self.colorscheme, color_box, self.scale], layout=Layout( grid_template_columns="repeat(3, 300px)") ) def choose_coloring_method(change: traitlets.utils.bunch.Bunch) -> None: if change.new == 'Scheme': self.colorscheme.disabled = False self.color_1.disabled = True self.color_2.disabled = True self.color_3.disabled = True elif change.new == 'Range': self.colorscheme.disabled = True self.color_1.disabled = False self.color_2.disabled = False self.color_3.disabled = False self.colorschemetype.observe(choose_coloring_method, names='value') # Grey out extent and maxbins widgets when binning is disabled def bin_options(change: traitlets.utils.bunch.Bunch) -> None: if change.new == 'Binned': self.maxbins.disabled = False self.extentmin.disabled = False self.extentmax.disabled = False self.scale.disabled = True self.colorschemetype.value = 'Scheme' self.colorschemetype.disabled = True else: self.maxbins.disabled = True self.extentmin.disabled = True self.extentmax.disabled = True self.scale.disabled = False self.colorschemetype.disabled = False self.bin.observe(bin_options, names='value') def get_altair_color_obj(self, data: pd.DataFrame, column: str) -> alt.Color: """Build color object for Altair plot from widget selections. Args: data: Pandas dataframe with the Altair chart data. column: column of source chart's data which contains the colour-encoded data. Returns: alt.Color object to be used by alt.Chart """ # If the bin checkbox selected if self.bin.value == 'Binned': # If not already set, set the default values of the extent widget to data min and max if self.extentmax.value == 0: self.extentmin.value = data[column].min() self.extentmax.value = data[column].max() # create the altair bin object from widget values bin = alt.Bin(maxbins=self.maxbins.value, extent=[self.extentmin.value, self.extentmax.value]) else: # set the bin var as False bool which alt.Color accepts bin = False # Depending on whether scheme or range selected, use different widgets to create the alt.Scale obj if self.colorschemetype.value == 'Scheme': # Only use the scale widget when bin not selected # (otherwise binning colour scale ignored in favour of continuous scale) if self.bin.value == 'Binned': scale = alt.Scale(scheme=self.colorscheme.value) else: scale = alt.Scale(type=self.scale.value, scheme=self.colorscheme.value) elif self.colorschemetype.value == 'Range': colorrange = [self.color_1.value, self.color_2.value, self.color_3.value ] # The below only looks right when bin is false (continuous scale). # Widgets have been set up so that self.colorschemetype.value is always 'Scheme' # when self.bin.value is 'Binned'. scale = alt.Scale(type=self.scale.value, range=colorrange) return alt.Color(column, legend=None, bin=bin, scale=scale) def display(self, data: pd.DataFrame, column: str, func: FuncT, custom_widgets: dict[str, Any] = {}) -> None: """Generate interactive plot from widgets and interactive plot function. Args: data: Pandas dataframe. column: column of data to be used for color binning. func: chart plotting function. custom_widgets: dictionary of string name keys and widget values. """ def interact_func(**kwargs: Any) -> None: """Interactive function that gets passed to widgets.interactive_output.""" # Use the WAYS widgets to generate the altair color object color = self.get_altair_color_obj(data, column) # Pass the data and color object into the chart func display(func(data, color)) # Get a dictionary of the widgets to be passed to the interactive function controls = {'bin': self.bin, 'maxbins': self.maxbins, 'extentmin': self.extentmin, 'extentmax': self.extentmax, 'scale': self.scale, 'colorschemetype': self.colorschemetype, 'colorscheme': self.colorscheme, 'color_1': self.color_1, 'color_2': self.color_2, 'color_3': self.color_3 } if custom_widgets: # Get a dictionary of the widgets to use as controls and add to the dictionary controls = custom_widgets | controls # Create a GridBox to arrange custom widgets into rows of three custom_widgets_grid = widgets.GridBox(list(custom_widgets.values()), layout=Layout(grid_template_columns="repeat(3, 300px)") ) # Use Jupyter widgets interactive_output to apply the control widgets to the interactive plot display(custom_widgets_grid, self.bin_grid, self.scale_grid, widgets.interactive_output(interact_func, controls)) else: display(self.bin_grid, self.scale_grid, widgets.interactive_output(interact_func, controls)) # Change the value of a widget so the plot auto-generates # Note: for some reason doing this once instead of twice results in duplicate plots... # TODO: may have to change this if there are scenarios where bin isn't used self.bin.value = 'Continuous' self.bin.value = 'Binned' def altair_color_widgets( custom_widgets: dict[str, Any] = {} ) -> Callable[[FuncT], Callable[[Any, str], None]]: """Widgets decorator for Altair colour binning, with option to add custom widgets. Args: custom_widgets: dictionary mapping names to widget values. """ def decorator(func: FuncT) -> Callable[[Any, str], None]: def wrapper(data: pd.DataFrame, column: str) -> None: if custom_widgets: # Add each custom widget to the colour widgets class for name, widget in custom_widgets.items(): setattr(AltairColorWidgets, name, widget) widgets = AltairColorWidgets() widgets.display(data, column, func, custom_widgets=custom_widgets) return wrapper return decorator
Module variables
var FuncT
Type variable for internal module use.
Functions
def altair_color_viz(
make_chart: ~FuncT)
Decorator which attaches an AltairColorViz meta-visualisation to a colour-encoded Altair chart.
Given a function which creates an Altair chart using an alt.Color object for colour encoding, adapt that function to return the original chart decorated with an AltairColorViz meta-visualisation.
def altair_color_viz(make_chart: FuncT) -> FuncT: """Decorator which attaches an AltairColorViz meta-visualisation to a colour-encoded Altair chart. Given a function which creates an Altair chart using an alt.Color object for colour encoding, adapt that function to return the original chart decorated with an AltairColorViz meta-visualisation. """ @wraps(make_chart) def wrapper(*args: Any, **kwargs: Any) -> Any: return AltairColorViz.decorate(make_chart(*args, **kwargs)) return cast(FuncT, wrapper)
def altair_color_widgets(
custom_widgets: dict[str, typing.Any] = {})
Widgets decorator for Altair colour binning, with option to add custom widgets.
Args: custom_widgets: dictionary mapping names to widget values.
def altair_color_widgets( custom_widgets: dict[str, Any] = {} ) -> Callable[[FuncT], Callable[[Any, str], None]]: """Widgets decorator for Altair colour binning, with option to add custom widgets. Args: custom_widgets: dictionary mapping names to widget values. """ def decorator(func: FuncT) -> Callable[[Any, str], None]: def wrapper(data: pd.DataFrame, column: str) -> None: if custom_widgets: # Add each custom widget to the colour widgets class for name, widget in custom_widgets.items(): setattr(AltairColorWidgets, name, widget) widgets = AltairColorWidgets() widgets.display(data, column, func, custom_widgets=custom_widgets) return wrapper return decorator
def is_defined(
v: Any)
Altair's notion of an undefined schema property.
def is_defined(v: Any) -> bool: """Altair's notion of an undefined schema property.""" return type(v).__name__ != 'UndefinedType'
Classes
class AltairColorViz
Meta-visualisation for alt.Color object.
Has two components: a plot of the underlying distribution of the chart, as a vertical histogram,
computed by density_chart
, and a plot of the colours used, which serves as the y-axis "labels",
computed by used_colours
.
class AltairColorViz: """Meta-visualisation for alt.Color object. Has two components: a plot of the underlying distribution of the chart, as a vertical histogram, computed by `density_chart`, and a plot of the colours used, which serves as the y-axis "labels", computed by `used_colours`. """ @staticmethod def _field(src: alt.Chart) -> str: """Centralise the assumption that this property stores just a field name.""" return cast(str, src.encoding.color.shorthand) @staticmethod def density_chart(src: alt.Chart) -> alt.Chart: """The underlying distribution of the chart as a histogram; placed alongside 'colours used'.""" if src.encoding.color.bin: if is_defined(src.encoding.color.bin.extent): extent = src.encoding.color.bin.extent bin = alt.Bin(maxbins=100, extent=extent) y_scale = alt.Scale(domain=extent, nice=True) else: bin = alt.Bin(maxbins=100) y_scale = alt.Scale(zero=False, nice=True) else: bin = alt.Bin(maxbins=100) y_scale = alt.Scale(nice=False) ys = src.data[AltairColorViz._field(src)] # assume src.data array-like in an appropriate way y_min, y_max = min(ys), max(ys) # tickCount/tickMinStep Axis properties are ignored (perhaps because we specify bins), so hard code y_axis = alt.Y( src.encoding.color.shorthand, bin=bin, axis=alt.Axis(orient='left', grid=False, values=sorted([0, 50] + [y_min, y_max])), title=src.encoding.color.shorthand, scale=y_scale ) x_axis = alt.X( 'sum(proportion):Q', sort='descending', axis=alt.Axis(grid=False), title="density" ) # Title for both the density_chart and used_colours plots title = "Colours used" return alt.Chart(src.data, title=title) \ .transform_joinaggregate(total='count(*)') \ .transform_calculate(proportion="1 / datum.total") \ .mark_bar(color='gray') \ .encode(y_axis, x_axis) \ .properties(width=100, height=300) @staticmethod def used_colours(src: alt.Chart) -> alt.Chart: """The colours used by the chart, plotted as another (vertical) chart.""" y_axis = alt.Axis(orient='right', grid=False) x_axis = alt.Axis(labels=False, tickSize=0, grid=False, titleAngle=270, titleAlign='right') if src.encoding.color.bin: if is_defined(src.encoding.color.bin.extent): extent = src.encoding.color.bin.extent y_scale = alt.Scale(domain=extent, nice=True) else: y_scale = alt.Scale(zero=False, nice=True) chart = alt.Chart(src.data) \ .mark_rect() \ .transform_bin( as_=['y', 'y2'], bin=src.encoding.color.bin, field=AltairColorViz._field(src) ) \ .transform_calculate(x='5') \ .encode( y=alt.Y('y:Q', axis=y_axis, title="", scale=y_scale), y2='y2:Q', x=alt.X('x:Q', sort='descending', axis=x_axis, title="") ) else: y_scale = alt.Scale(nice=False) # Get a dataframe to plot where there is only one row for each unique value # of the column of the source chart data being plotted df = src.data.drop_duplicates(subset=[src.encoding.color.shorthand]) chart = alt.Chart(df) \ .mark_bar() \ .encode( y=alt.Y(src.encoding.color.shorthand, axis=y_axis, title="", scale=y_scale), x=alt.X('count()', sort='descending', axis=x_axis, title="") ) return chart.encode(src.encoding.color) \ .properties(width=20, height=300) @staticmethod def decorate(src: alt.Chart) -> alt.Chart: """Decorate a colour-ended Altair chart with meta-visualisations showing how the colours are used. Args: src: colour-encoded Altair chart to be decorated. Returns: Altair chart object: modified chart """ if not is_defined(src.encoding.color.bin): raise Exception("Can only apply decorator to chart with color.bin defined.") meta_chart: alt.Chart = (AltairColorViz.density_chart(src) | AltairColorViz.used_colours(src)) return (meta_chart | src) \ .configure_view(strokeWidth=0) \ .configure_concat(spacing=5)
Static methods
def decorate(
src: altair.vegalite.v4.api.Chart)
Decorate a colour-ended Altair chart with meta-visualisations showing how the colours are used.
Args: src: colour-encoded Altair chart to be decorated.
Returns: Altair chart object: modified chart
@staticmethod def decorate(src: alt.Chart) -> alt.Chart: """Decorate a colour-ended Altair chart with meta-visualisations showing how the colours are used. Args: src: colour-encoded Altair chart to be decorated. Returns: Altair chart object: modified chart """ if not is_defined(src.encoding.color.bin): raise Exception("Can only apply decorator to chart with color.bin defined.") meta_chart: alt.Chart = (AltairColorViz.density_chart(src) | AltairColorViz.used_colours(src)) return (meta_chart | src) \ .configure_view(strokeWidth=0) \ .configure_concat(spacing=5)
def density_chart(
src: altair.vegalite.v4.api.Chart)
The underlying distribution of the chart as a histogram; placed alongside 'colours used'.
@staticmethod def density_chart(src: alt.Chart) -> alt.Chart: """The underlying distribution of the chart as a histogram; placed alongside 'colours used'.""" if src.encoding.color.bin: if is_defined(src.encoding.color.bin.extent): extent = src.encoding.color.bin.extent bin = alt.Bin(maxbins=100, extent=extent) y_scale = alt.Scale(domain=extent, nice=True) else: bin = alt.Bin(maxbins=100) y_scale = alt.Scale(zero=False, nice=True) else: bin = alt.Bin(maxbins=100) y_scale = alt.Scale(nice=False) ys = src.data[AltairColorViz._field(src)] # assume src.data array-like in an appropriate way y_min, y_max = min(ys), max(ys) # tickCount/tickMinStep Axis properties are ignored (perhaps because we specify bins), so hard code y_axis = alt.Y( src.encoding.color.shorthand, bin=bin, axis=alt.Axis(orient='left', grid=False, values=sorted([0, 50] + [y_min, y_max])), title=src.encoding.color.shorthand, scale=y_scale ) x_axis = alt.X( 'sum(proportion):Q', sort='descending', axis=alt.Axis(grid=False), title="density" ) # Title for both the density_chart and used_colours plots title = "Colours used" return alt.Chart(src.data, title=title) \ .transform_joinaggregate(total='count(*)') \ .transform_calculate(proportion="1 / datum.total") \ .mark_bar(color='gray') \ .encode(y_axis, x_axis) \ .properties(width=100, height=300)
def used_colours(
src: altair.vegalite.v4.api.Chart)
The colours used by the chart, plotted as another (vertical) chart.
@staticmethod def used_colours(src: alt.Chart) -> alt.Chart: """The colours used by the chart, plotted as another (vertical) chart.""" y_axis = alt.Axis(orient='right', grid=False) x_axis = alt.Axis(labels=False, tickSize=0, grid=False, titleAngle=270, titleAlign='right') if src.encoding.color.bin: if is_defined(src.encoding.color.bin.extent): extent = src.encoding.color.bin.extent y_scale = alt.Scale(domain=extent, nice=True) else: y_scale = alt.Scale(zero=False, nice=True) chart = alt.Chart(src.data) \ .mark_rect() \ .transform_bin( as_=['y', 'y2'], bin=src.encoding.color.bin, field=AltairColorViz._field(src) ) \ .transform_calculate(x='5') \ .encode( y=alt.Y('y:Q', axis=y_axis, title="", scale=y_scale), y2='y2:Q', x=alt.X('x:Q', sort='descending', axis=x_axis, title="") ) else: y_scale = alt.Scale(nice=False) # Get a dataframe to plot where there is only one row for each unique value # of the column of the source chart data being plotted df = src.data.drop_duplicates(subset=[src.encoding.color.shorthand]) chart = alt.Chart(df) \ .mark_bar() \ .encode( y=alt.Y(src.encoding.color.shorthand, axis=y_axis, title="", scale=y_scale), x=alt.X('count()', sort='descending', axis=x_axis, title="") ) return chart.encode(src.encoding.color) \ .properties(width=20, height=300)
class AltairColorWidgets
WAYS widgets class for Altair color object.
class AltairColorWidgets: """WAYS widgets class for Altair color object.""" def __init__(self) -> None: """Create Jupyter widgets that can be used as input to Altair color object in a Jupyter notebook.""" # Checkbox widget that determines whether binning is enabled self.bin = widgets.RadioButtons(value='Binned', options=['Binned', 'Continuous'], description='Color Binning') # Textbox accepting integer to select the maximum number of bins self.maxbins = widgets.IntText(value=7, description='Max Bins:', continuous_update=True) # Two widgets determining where the binning of data starts and ends self.extentmin = widgets.IntText(value=0, continuous_update=True, description='Extent Min') self.extentmax = widgets.IntText(value=0, continuous_update=True, description='Extent Max') wide_Vbox = Layout(display='flex', flex_flow='column', align_items='center', width='110%') self.extent = Box(children=[self.extentmin, self.extentmax], layout=wide_Vbox) # Create a horizontal box that contains these widgets self.bin_grid = widgets.GridBox([self.bin, self.maxbins, self.extent], layout=Layout( grid_template_columns="repeat(3, 300px)") ) # list of scales from: # https://altair-viz.github.io/user_guide/generated/core/altair.ScaleType.html#altair.ScaleType scales = ['linear', 'log', 'pow', 'sqrt', 'symlog', 'identity', 'sequential', 'time', 'utc', 'quantile', 'quantize', 'threshold', 'bin-ordinal', 'ordinal', 'point', 'band'] self.scale = widgets.Dropdown(value='linear', options=scales, description='Color Scale') # list from https://vega.github.io/vega/docs/schemes/#reference schemes = ['blues', 'tealblues', 'teals', 'greens', 'browns', 'oranges', 'reds', 'purples', 'warmgreys', 'greys', 'viridis', 'magma', 'inferno', 'plasma', 'cividis', 'turbo', 'bluegreen', 'bluepurple', 'goldgreen', 'goldorange', 'goldred', 'greenblue', 'orangered', 'purplebluegreen', 'purpleblue', 'purplered', 'redpurple', 'yellowgreenblue', 'yellowgreen', 'yelloworangebrown', 'yelloworangered', 'darkblue', 'darkgold', 'darkgreen', 'darkmulti', 'darkred', 'lightgreyred', 'lightgreyteal', 'lightmulti', 'lightorange', 'lighttealblue', 'blueorange', 'brownbluegreen', 'purplegreen', 'pinkyellowgreen', 'purpleorange', 'redblue', 'redgrey', 'redyellowblue', 'redyellowgreen', 'spectral', 'rainbow', 'sinebow'] # The widgets here expose a variety of options for setting the color scheme: # colorscheme and the color range boxes are greyed out when not selected by colorschemetype self.colorschemetype = widgets.RadioButtons(value='Scheme', options=['Scheme', 'Range'], description='Color Method') self.colorscheme = widgets.Dropdown(options=schemes, description='Scheme') self.color_1 = widgets.ColorPicker(concise=True, value='red', disabled=True, description='Range') self.color_2 = widgets.ColorPicker(concise=True, value='purple', disabled=True) self.color_3 = widgets.ColorPicker(concise=True, value='blue', disabled=True) wide_Hbox = Layout(display='flex', flex_flow='row', align_items='center', width='110%') color_box = Box([self.color_1, self.color_2, self.color_3], layout=wide_Hbox) self.scale_grid = widgets.GridBox([self.colorschemetype, self.colorscheme, color_box, self.scale], layout=Layout( grid_template_columns="repeat(3, 300px)") ) def choose_coloring_method(change: traitlets.utils.bunch.Bunch) -> None: if change.new == 'Scheme': self.colorscheme.disabled = False self.color_1.disabled = True self.color_2.disabled = True self.color_3.disabled = True elif change.new == 'Range': self.colorscheme.disabled = True self.color_1.disabled = False self.color_2.disabled = False self.color_3.disabled = False self.colorschemetype.observe(choose_coloring_method, names='value') # Grey out extent and maxbins widgets when binning is disabled def bin_options(change: traitlets.utils.bunch.Bunch) -> None: if change.new == 'Binned': self.maxbins.disabled = False self.extentmin.disabled = False self.extentmax.disabled = False self.scale.disabled = True self.colorschemetype.value = 'Scheme' self.colorschemetype.disabled = True else: self.maxbins.disabled = True self.extentmin.disabled = True self.extentmax.disabled = True self.scale.disabled = False self.colorschemetype.disabled = False self.bin.observe(bin_options, names='value') def get_altair_color_obj(self, data: pd.DataFrame, column: str) -> alt.Color: """Build color object for Altair plot from widget selections. Args: data: Pandas dataframe with the Altair chart data. column: column of source chart's data which contains the colour-encoded data. Returns: alt.Color object to be used by alt.Chart """ # If the bin checkbox selected if self.bin.value == 'Binned': # If not already set, set the default values of the extent widget to data min and max if self.extentmax.value == 0: self.extentmin.value = data[column].min() self.extentmax.value = data[column].max() # create the altair bin object from widget values bin = alt.Bin(maxbins=self.maxbins.value, extent=[self.extentmin.value, self.extentmax.value]) else: # set the bin var as False bool which alt.Color accepts bin = False # Depending on whether scheme or range selected, use different widgets to create the alt.Scale obj if self.colorschemetype.value == 'Scheme': # Only use the scale widget when bin not selected # (otherwise binning colour scale ignored in favour of continuous scale) if self.bin.value == 'Binned': scale = alt.Scale(scheme=self.colorscheme.value) else: scale = alt.Scale(type=self.scale.value, scheme=self.colorscheme.value) elif self.colorschemetype.value == 'Range': colorrange = [self.color_1.value, self.color_2.value, self.color_3.value ] # The below only looks right when bin is false (continuous scale). # Widgets have been set up so that self.colorschemetype.value is always 'Scheme' # when self.bin.value is 'Binned'. scale = alt.Scale(type=self.scale.value, range=colorrange) return alt.Color(column, legend=None, bin=bin, scale=scale) def display(self, data: pd.DataFrame, column: str, func: FuncT, custom_widgets: dict[str, Any] = {}) -> None: """Generate interactive plot from widgets and interactive plot function. Args: data: Pandas dataframe. column: column of data to be used for color binning. func: chart plotting function. custom_widgets: dictionary of string name keys and widget values. """ def interact_func(**kwargs: Any) -> None: """Interactive function that gets passed to widgets.interactive_output.""" # Use the WAYS widgets to generate the altair color object color = self.get_altair_color_obj(data, column) # Pass the data and color object into the chart func display(func(data, color)) # Get a dictionary of the widgets to be passed to the interactive function controls = {'bin': self.bin, 'maxbins': self.maxbins, 'extentmin': self.extentmin, 'extentmax': self.extentmax, 'scale': self.scale, 'colorschemetype': self.colorschemetype, 'colorscheme': self.colorscheme, 'color_1': self.color_1, 'color_2': self.color_2, 'color_3': self.color_3 } if custom_widgets: # Get a dictionary of the widgets to use as controls and add to the dictionary controls = custom_widgets | controls # Create a GridBox to arrange custom widgets into rows of three custom_widgets_grid = widgets.GridBox(list(custom_widgets.values()), layout=Layout(grid_template_columns="repeat(3, 300px)") ) # Use Jupyter widgets interactive_output to apply the control widgets to the interactive plot display(custom_widgets_grid, self.bin_grid, self.scale_grid, widgets.interactive_output(interact_func, controls)) else: display(self.bin_grid, self.scale_grid, widgets.interactive_output(interact_func, controls)) # Change the value of a widget so the plot auto-generates # Note: for some reason doing this once instead of twice results in duplicate plots... # TODO: may have to change this if there are scenarios where bin isn't used self.bin.value = 'Continuous' self.bin.value = 'Binned'
Methods
def display(
self, data: pandas.core.frame.DataFrame, column: str, func: ~FuncT, custom_widgets: dict[str, typing.Any] = {})
Generate interactive plot from widgets and interactive plot function.
Args: data: Pandas dataframe. column: column of data to be used for color binning. func: chart plotting function. custom_widgets: dictionary of string name keys and widget values.
def display(self, data: pd.DataFrame, column: str, func: FuncT, custom_widgets: dict[str, Any] = {}) -> None: """Generate interactive plot from widgets and interactive plot function. Args: data: Pandas dataframe. column: column of data to be used for color binning. func: chart plotting function. custom_widgets: dictionary of string name keys and widget values. """ def interact_func(**kwargs: Any) -> None: """Interactive function that gets passed to widgets.interactive_output.""" # Use the WAYS widgets to generate the altair color object color = self.get_altair_color_obj(data, column) # Pass the data and color object into the chart func display(func(data, color)) # Get a dictionary of the widgets to be passed to the interactive function controls = {'bin': self.bin, 'maxbins': self.maxbins, 'extentmin': self.extentmin, 'extentmax': self.extentmax, 'scale': self.scale, 'colorschemetype': self.colorschemetype, 'colorscheme': self.colorscheme, 'color_1': self.color_1, 'color_2': self.color_2, 'color_3': self.color_3 } if custom_widgets: # Get a dictionary of the widgets to use as controls and add to the dictionary controls = custom_widgets | controls # Create a GridBox to arrange custom widgets into rows of three custom_widgets_grid = widgets.GridBox(list(custom_widgets.values()), layout=Layout(grid_template_columns="repeat(3, 300px)") ) # Use Jupyter widgets interactive_output to apply the control widgets to the interactive plot display(custom_widgets_grid, self.bin_grid, self.scale_grid, widgets.interactive_output(interact_func, controls)) else: display(self.bin_grid, self.scale_grid, widgets.interactive_output(interact_func, controls)) # Change the value of a widget so the plot auto-generates # Note: for some reason doing this once instead of twice results in duplicate plots... # TODO: may have to change this if there are scenarios where bin isn't used self.bin.value = 'Continuous' self.bin.value = 'Binned'
def get_altair_color_obj(
self, data: pandas.core.frame.DataFrame, column: str)
Build color object for Altair plot from widget selections.
Args: data: Pandas dataframe with the Altair chart data. column: column of source chart's data which contains the colour-encoded data.
Returns: alt.Color object to be used by alt.Chart
def get_altair_color_obj(self, data: pd.DataFrame, column: str) -> alt.Color: """Build color object for Altair plot from widget selections. Args: data: Pandas dataframe with the Altair chart data. column: column of source chart's data which contains the colour-encoded data. Returns: alt.Color object to be used by alt.Chart """ # If the bin checkbox selected if self.bin.value == 'Binned': # If not already set, set the default values of the extent widget to data min and max if self.extentmax.value == 0: self.extentmin.value = data[column].min() self.extentmax.value = data[column].max() # create the altair bin object from widget values bin = alt.Bin(maxbins=self.maxbins.value, extent=[self.extentmin.value, self.extentmax.value]) else: # set the bin var as False bool which alt.Color accepts bin = False # Depending on whether scheme or range selected, use different widgets to create the alt.Scale obj if self.colorschemetype.value == 'Scheme': # Only use the scale widget when bin not selected # (otherwise binning colour scale ignored in favour of continuous scale) if self.bin.value == 'Binned': scale = alt.Scale(scheme=self.colorscheme.value) else: scale = alt.Scale(type=self.scale.value, scheme=self.colorscheme.value) elif self.colorschemetype.value == 'Range': colorrange = [self.color_1.value, self.color_2.value, self.color_3.value ] # The below only looks right when bin is false (continuous scale). # Widgets have been set up so that self.colorschemetype.value is always 'Scheme' # when self.bin.value is 'Binned'. scale = alt.Scale(type=self.scale.value, range=colorrange) return alt.Color(column, legend=None, bin=bin, scale=scale)