diff --git a/curvesim/plot/altair/chart_properties.py b/curvesim/plot/altair/chart_properties.py index d7ca6fdef..5c7461ff9 100644 --- a/curvesim/plot/altair/chart_properties.py +++ b/curvesim/plot/altair/chart_properties.py @@ -1,3 +1,9 @@ +""" +This module provides functionality for creating and manipulating chart properties. + +It contains utility functions for creating keyword arguments for a chart, updating +and initializing chart properties, and handling property-specific behaviors. +""" from copy import deepcopy from functools import partial @@ -5,6 +11,23 @@ def make_chart_kwargs(default, override_dicts): + """ + Create the keyword arguments for a chart by updating a default dictionary with + values from a list of override dictionaries. + + Parameters + ---------- + default : dict + The default keyword arguments for the chart. + override_dicts : list + A list of dictionaries containing addtional keyword arguments and/or overrides + for the default keyword arguments. + + Returns + ------- + dict + A dictionary containing the keyword arguments for the chart. + """ chart_kwargs = deepcopy(default) for d in override_dicts: @@ -16,6 +39,25 @@ def make_chart_kwargs(default, override_dicts): def update_properties(prop_dict, key, val, depth=0, count=0): + """ + Update properties in a dictionary. If the current depth matches the target depth, + the value is updated directly. Otherwise, the function is called recursively on + nested dictionaries. + + Parameters + ---------- + prop_dict : dict + The dictionary of properties to update. + key : str + The key of the property to update. + val : any + The new value for the property. + depth : int, optional + The target depth for the update. Defaults to 0. + count : int, optional + The current depth of the update. Defaults to 0. + """ + if depth in [0, count]: prop_dict[key] = val else: @@ -25,6 +67,19 @@ def update_properties(prop_dict, key, val, depth=0, count=0): def concat_properties(prop_dict, key, val): + """ + Concatenate values to a property in a dictionary. If the value is a list, extend + the property with it. Otherwise, append the value to the property. + + Parameters + ---------- + prop_dict : dict + The dictionary of properties to update. + key : str + The key of the property to update. + val : any + The value to concatenate to the property. + """ prop_dict.setdefault(key, []) if isinstance(val, list): @@ -34,10 +89,44 @@ def concat_properties(prop_dict, key, val): def ignore_property(prop_dict, key, val): # pylint: disable=unused-argument - pass + """ + Ignore a property during an update operation. This function does nothing and is + intended to be used as a placeholder in UPDATE_RULES where certain properties + should be ignored. + + Parameters + ---------- + prop_dict : dict + The dictionary of properties to update. + key : str + The key of the property to ignore. + val : any + The value of the property to ignore. + """ def init_properties(prop_dict, classes=None): + """ + Initialize the properties of a dictionary using a mapping of classes. If a + property's key is present in the classes dictionary, its value is replaced + with an instance of the corresponding class, initialized with the value as + keyword arguments. + + Parameters + ---------- + prop_dict : dict + The dictionary of properties to initialize. + classes : dict, optional + A dictionary mapping keys to classes. If a key in prop_dict matches a + key in classes, the value in prop_dict is replaced with an instance of + the corresponding class. Defaults to PROPERTY_CLASSES. + + Returns + ------- + dict + The dictionary of properties, with values replaced with class instances + where applicable. + """ classes = classes or PROPERTY_CLASSES for key, val in prop_dict.items(): try: diff --git a/curvesim/plot/altair/make_chart.py b/curvesim/plot/altair/make_chart.py index c6a5d5714..d4ef184ad 100644 --- a/curvesim/plot/altair/make_chart.py +++ b/curvesim/plot/altair/make_chart.py @@ -1,14 +1,47 @@ -from altair import Scale, Chart, data_transformers +""" +This module provides functionality for creating Altair charts with custom styles. + +It contains utility functions for creating charts and default chart properties. It +uses styles defined in the .styles module and allows custom configuration of chart +properties. +""" +from altair import Chart, Scale, data_transformers from curvesim.exceptions import PlotError + from .chart_properties import make_chart_kwargs from .styles import STYLES - data_transformers.disable_max_rows() def make_chart(config, x=None, y=None, color=None, **kwargs): + """ + Create an interactive chart with custom properties and styles. + + Parameters + ---------- + config : dict + The configuration dictionary for the chart. Must contain a 'style' key. + x : str or dict, optional + The x-axis property. Can be a shorthand string or a dictionary of alt.X kwargs. + y : str or dict, optional + The y-axis property. Can be a shorthand string or a dictionary of alt.Y kwargs. + color : str or dict, optional + The color property. Can be a shorthand string or a dictionary of alt.Color kwargs. + **kwargs + Additional keyword arguments are added to the chart configuration. + + Returns + ------- + altair.Chart + The created chart. + + Raises + ------ + PlotError + If no style is found in the config or if the style is not found in STYLES. + """ title = config.get("title", "") try: @@ -26,7 +59,31 @@ def make_chart(config, x=None, y=None, color=None, **kwargs): return Chart(**chart_kwargs).interactive() -def make_defaults(title, x, y, color): +def make_defaults(title, x=None, y=None, color=None): + """ + Create default properties for a chart. + + Parameters + ---------- + title : str + The title of the chart. + x : str or dict, optional + The x-axis property. Can be a shorthand string or a dictionary of alt.X kwargs. + y : str or dict, optional + The y-axis property. Can be a shorthand string or a dictionary of alt.Y kwargs. + color : str or dict, optional + The color property. Can be a shorthand string or a dictionary of alt.Color kwargs. + + Returns + ------- + dict + The default properties for a chart. + + Raises + ------ + PlotError + If x, y, or color is not a string (shorthand) or a dictionary (altair kwargs). + """ defaults = { "title": title, "encoding": { diff --git a/curvesim/plot/altair/results/__init__.py b/curvesim/plot/altair/results/__init__.py index e69de29bb..568e2edcb 100644 --- a/curvesim/plot/altair/results/__init__.py +++ b/curvesim/plot/altair/results/__init__.py @@ -0,0 +1 @@ +"""Submodule with resources for plotting simulation results with Altair.""" diff --git a/curvesim/plot/altair/results/make_page.py b/curvesim/plot/altair/results/make_page.py index e221df23e..7658a4e39 100644 --- a/curvesim/plot/altair/results/make_page.py +++ b/curvesim/plot/altair/results/make_page.py @@ -1,3 +1,11 @@ +""" +This module provides functionality for creating interactive Altair charts from +simulation results. + +It contains utility functions for creating data dictionaries, pages, subplots, +and layered or single charts. It uses selectors to control chart properties and +apply tooltips. +""" from altair import concat, data_transformers, layer, value, vconcat from ..make_chart import make_chart @@ -10,6 +18,25 @@ def make_page_from_results(results, data_key, axes, downsample=False): + """ + Creates an interactive Altair chart from simulation results. + + Parameters + ---------- + results : Results + The simulation results. + data_key : str + The key to retrieve data from the results. + axes : dict + The axes configuration. + downsample : bool, optional + Whether to downsample the data. Defaults to False. + + Returns + ------- + altair.vconcat + The created chart page. + """ config = results.plot_config[data_key] factor_dict = results.factors factor_keys = list(factor_dict.keys()) @@ -20,12 +47,54 @@ def make_page_from_results(results, data_key, axes, downsample=False): def make_data_dict(results, data_key, config, factor_keys, downsample): + """ + Prepares the data from the results for chart creation. + + Parameters + ---------- + results : Results + The simulation results. + data_key : str + The key to retrieve data from the results. + config : dict + The plot configuration. + factor_keys : list + The keys for the factors. + downsample : bool + Whether to downsample the data. + + Returns + ------- + dict + The data dictionary. + """ columns = factor_keys + list(config.keys()) data = getattr(results, data_key)(columns=columns) return preprocess_data(data, config, factor_keys, downsample) def make_page(data_dict, config, factors, metric_axis, selectors): + """ + Creates an interactive Altair chart page with multiple subplots. + + Parameters + ---------- + data_dict : dict + The data dictionary. + config : dict + The plot configuration. + factors : list + The list of factors. + metric_axis : str + The axis for the metrics. + selectors : dict + The selectors configuration. + + Returns + ------- + altair.vconcat + The created chart page. + """ kwargs = selectors["kwargs"] charts = concat(data=data_dict["main"], columns=2) @@ -39,6 +108,21 @@ def make_page(data_dict, config, factors, metric_axis, selectors): def get_metric_data(metric_key, data_dict): + """ + Retrieves the relevant data for a specific metric. + + Parameters + ---------- + metric_key : str + The key for the metric. + data_dict : dict + The data dictionary. + + Returns + ------- + str or list of str, pandas.DataFrame + The metrics and the data for the metrics. + """ if metric_key in data_dict: data = data_dict[metric_key] else: @@ -57,6 +141,27 @@ def get_metric_data(metric_key, data_dict): def make_subplot(config, metrics, factors, metric_axis, kwargs): + """ + Creates a subplot for a specific metric. + + Parameters + ---------- + config : dict + The plot configuration. + metrics : str or list of str + The metric or metrics. + factors : list + The list of factors. + metric_axis : str + The axis for the metrics. + kwargs : dict + Additional keyword arguments. + + Returns + ------- + altair.vconcat + The created subplot. + """ if isinstance(metrics, list): make = make_layered_chart else: @@ -66,6 +171,27 @@ def make_subplot(config, metrics, factors, metric_axis, kwargs): def make_single_chart(config, metric, metric_axis, factors, **kwargs): + """ + Creates a single chart for a specific metric. + + Parameters + ---------- + config : dict + The plot configuration. + metric : str + The metric. + metric_axis : str + The axis for the metrics. + factors : list + The list of factors. + **kwargs + Additional keyword arguments. + + Returns + ------- + altair.Chart + The created chart. + """ kwargs[metric_axis] = metric chart = make_chart(config, **kwargs) tooltip = make_tooltip(chart["encoding"], metric_axis, factors) @@ -73,6 +199,27 @@ def make_single_chart(config, metric, metric_axis, factors, **kwargs): def make_layered_chart(config, metrics, metric_axis, factors, **kwargs): + """ + Creates a layered chart for multiple submetrics of a metric. + + Parameters + ---------- + config : dict + The plot configuration. + metrics : list of str + The metrics. + metric_axis : str + The axis for the metrics. + factors : list + The list of factors. + **kwargs + Additional keyword arguments. + + Returns + ------- + altair.vconcat + The created chart. + """ labels = [metric.split(" ")[1].capitalize() for metric in metrics] sel_chart, selector = make_selector( "submetric", metrics, labels=labels, toggle="true", sel_idx=0 diff --git a/curvesim/plot/altair/results/preprocessing.py b/curvesim/plot/altair/results/preprocessing.py index 0cbb7da4b..5c34d2525 100644 --- a/curvesim/plot/altair/results/preprocessing.py +++ b/curvesim/plot/altair/results/preprocessing.py @@ -1,9 +1,34 @@ +""" +This module provides functionality for preprocessing data for chart creation. + +It contains utility functions for downsampling and preprocessing data, resampling data, +converting data to histograms, and creating histograms. +""" from pandas import Grouper, Series, concat from curvesim.exceptions import PlotError def preprocess_data(data, config, factors, downsample=False): + """ + Preprocesses data for chart creation, with an option to downsample. + + Parameters + ---------- + data : pandas.DataFrame + The data to preprocess. + config : dict + The plot configuration. + factors : list + The list of factors. + downsample : bool, optional + Whether to downsample the data. Defaults to False. + + Returns + ------- + dict + The preprocessed data dictionary. + """ if downsample: return downsample_data(data, config, factors) else: @@ -11,6 +36,28 @@ def preprocess_data(data, config, factors, downsample=False): def downsample_data(data, config, factors): + """ + Downscales the data according to provided configurations and factors. + + Parameters + ---------- + data : pandas.DataFrame + The data to downsample. + config : dict + The plot configuration. + factors : list + The list of factors. + + Returns + ------- + dict + The downsampled data dictionary. + + Raises + ------ + PlotError + If no resample strategy is found in the configuration for a metric. + """ data_out = {} resample = {"run": "last"} for metric, cfg in config.items(): @@ -34,16 +81,63 @@ def downsample_data(data, config, factors): def resample_data(data, factors, fns): + """ + Resamples the data based on the provided factors and resampling functions. + + Parameters + ---------- + data : pandas.DataFrame + The data to resample. + factors : list + The list of factors. + fns : dict + The resampling functions. + + Returns + ------- + pandas.DataFrame + The resampled data. + """ groups = factors + [Grouper(freq="1D", key="timestamp")] resampled = data.groupby(groups).agg(fns).round(6) return resampled.reset_index() def to_histograms(data, factors): + """ + Converts the data to histograms. + + Parameters + ---------- + data : pandas.DataFrame + The data to convert. + factors : list + The list of factors. + + Returns + ------- + pandas.DataFrame + The data converted to histograms. + """ return data.groupby(factors).apply(make_histogram).reset_index() def make_histogram(data, bins=500): + """ + Creates a histogram from the data. + + Parameters + ---------- + data : pandas.DataFrame + The data to create a histogram from. + bins : int, optional + The number of bins for the histogram. Defaults to 500. + + Returns + ------- + pandas.Series + The histogram. + """ metric = data.iloc[:, -1] minimum = Series(0, index=[metric.min()]) diff --git a/curvesim/plot/altair/results/result_selectors.py b/curvesim/plot/altair/results/result_selectors.py index 451706254..b0ccfc5cc 100644 --- a/curvesim/plot/altair/results/result_selectors.py +++ b/curvesim/plot/altair/results/result_selectors.py @@ -1,16 +1,33 @@ -from altair import ( - Color, - CalculateTransform, - FilterTransform, - Scale, - concat, - hconcat, -) +""" +This module provides functionality for creating result selectors for an Altair +chart. + +It contains utility functions for creating axis selectors, parameter filters, +selector charts and result selectors. +""" + +from altair import CalculateTransform, Color, FilterTransform, Scale, concat, hconcat from ..selectors import make_selector def make_result_selectors(factors, dynamic_axes): + """ + Creates result selectors for an Altair chart. + + Parameters + ---------- + factors : dict + The factors to create selectors for. + dynamic_axes : dict + The dynamic axes to create selectors for. + + Returns + ------- + dict + Keyword arguments to add selections to metric charts and chart objects to + display the selectors. + """ dynamic_axes = dict(list(dynamic_axes.items())[: len(factors)]) axes = make_axis_selectors(factors, dynamic_axes) @@ -30,6 +47,21 @@ def make_result_selectors(factors, dynamic_axes): def make_axis_selectors(factors, dynamic_axes): + """ + Creates axis selectors. + + Parameters + ---------- + factors : dict + The factors to use as selector options. + dynamic_axes : dict + The dynamic axes to create selectors for. + + Returns + ------- + dict + The axis selectors and the charts to display them. + """ factor_names = list(factors.keys()) charts = [] @@ -44,6 +76,21 @@ def make_axis_selectors(factors, dynamic_axes): def make_parameter_filters(factors, dynamic_axes): + """ + Creates parameter filters. + + Parameters + ---------- + factors : dict + The factors to create selectors for. + dynamic_axes : dict + The dynamic axes for a chart page. Used to determine initial selections. + + Returns + ------- + dict + The parameter filters and the charts to display them. + """ n_dynamic_axes = len(dynamic_axes) charts = [] @@ -59,6 +106,24 @@ def make_parameter_filters(factors, dynamic_axes): def _make_axis_selector(axis, options, sel_idx): + """ + Makes a single axis selector. + + Parameters + ---------- + axis : str + The axis to create a selector for. + options : list + The options for the selector. + sel_idx : int or 'all' + The initial selection index. If an integer, select that option. If 'all', + select all options. + + Returns + ------- + altair.Chart, altair.CalculateTransform + The axis selector chart and the transform for the selector. + """ chart, selector = make_selector( axis, options, @@ -73,6 +138,24 @@ def _make_axis_selector(axis, options, sel_idx): def _make_parameter_filter(factor, options, sel_idx): + """ + Makes a single parameter filter. + + Parameters + ---------- + factor : str + The factor to create a filter for. + options : list + The options for the filter. + sel_idx : int or 'all' + The initial selection index. If an integer, select that option. If 'all', + select all options. + + Returns + ------- + altair.Chart, altair.FilterTransform + The parameter filter chart and the filter transform. + """ color = Color("labels:O", scale=Scale(scheme="viridis"), legend=None) chart, selector = make_selector( @@ -87,6 +170,21 @@ def _make_parameter_filter(factor, options, sel_idx): def format_selector_charts(axis_selector_charts, parameter_filter_charts): + """ + Formats selector charts for display. + + Parameters + ---------- + axis_selector_charts : list of altair.Chart + The axis selector charts. + parameter_filter_charts : list of altair.Chart + The parameter filter charts. + + Returns + ------- + altair.hconcat + The formatted selector charts. + """ left = concat(*axis_selector_charts, title="Axis Selectors:") right = concat(*parameter_filter_charts, title="Toggle Filters:") return hconcat(left, right.resolve_scale(color="independent")) diff --git a/curvesim/plot/altair/results/tooltip.py b/curvesim/plot/altair/results/tooltip.py index 4239aad60..70e9a6882 100644 --- a/curvesim/plot/altair/results/tooltip.py +++ b/curvesim/plot/altair/results/tooltip.py @@ -2,6 +2,7 @@ def make_tooltip(encoding, metric_axis, factors, prefix=None): + """Makes a tooltip for a subplot.""" tooltip = [] if "timestamp" in encoding["x"]["shorthand"]: tooltip.append(Tooltip(encoding["x"]["shorthand"], title="Time")) diff --git a/curvesim/plot/altair/selectors.py b/curvesim/plot/altair/selectors.py index bc4aea654..934c2851e 100644 --- a/curvesim/plot/altair/selectors.py +++ b/curvesim/plot/altair/selectors.py @@ -1,7 +1,15 @@ +""" +This module provides functionality for creating interactive selector charts with +Altair and pandas. + +It contains utility functions for creating selector charts, initializing selectors, +manipulating opacity of selections, and conditionally applying properties. +""" from altair import Axis, Chart, Color, Scale, condition, selection_point, value from pandas import DataFrame from curvesim.exceptions import PlotError + from .chart_properties import PROPERTY_CLASSES @@ -15,6 +23,39 @@ def make_selector( toggle=True, sel_idx=None, ): + """ + Create an interactive selector chart and an Altair selection object. + + Parameters + ---------- + field : str + The field in the data to bind the selection to. + options : list + The options for the selection. + labels : list, optional + The labels for the options. Defaults to the options themselves. + title : str, optional + The title of the chart. Defaults to an empty string. + style : dict, optional + A dictionary of style options to update the default style with. + toggle : bool, optional + Whether to allow toggle behavior in the selection. Defaults to True. + sel_idx : int or 'all', optional + The initial selection index. If an integer, select that option. If 'all', + select all options. If None, no initial selection. Defaults to None. + + Returns + ------- + altair.Chart + The created selector chart. + altair.Selection + The created selection object. + + Raises + ------ + PlotError + If sel_idx is not None, an integer, or 'all'. + """ title = title or "" labels = labels or options @@ -29,6 +70,30 @@ def make_selector( def get_initial_selection(field, options, sel_idx): + """ + Get the initial selection value based on provided options and an index. + + Parameters + ---------- + field : str + The field in the data to bind the selection to. + options : list + The options for the selection. + sel_idx : int or 'all', optional + The initial selection index. If an integer, select that option. If 'all', + select all options. If None, no initial selection. + + Returns + ------- + dict or list of dict or None + The initial selection value. If sel_idx is an integer, a dictionary. If 'all', + a list of dictionaries. If None, None. + + Raises + ------ + PlotError + If sel_idx is not None, an integer, or 'all'. + """ if sel_idx is None: init_sel = None @@ -45,6 +110,27 @@ def get_initial_selection(field, options, sel_idx): def make_selector_chart(field, options, opt_labels, selector, style=None): + """ + Create the selector chart. + + Parameters + ---------- + field : str + The field in the data to bind the selection to. + options : list + The options for the selection. + opt_labels : list + The labels for the options. + selector : altair.Selection + The selector to add to the chart. + style : dict, optional + A dictionary of style options to update the default style with. + + Returns + ------- + altair.Chart + The created selector chart. + """ _style = { "axis": "y", "orient": "right", @@ -69,6 +155,27 @@ def make_selector_chart(field, options, opt_labels, selector, style=None): def if_selected(selected, selector, field, if_true, if_false): + """ + Apply a property if a selection is active. + + Parameters + ---------- + selected : any + The selected value. + selector : altair.Selection + The selector to check. + field : str + The field in the data to check the selection against. + if_true : any + The value to return if the selection is active. + if_false : any + The value to return if the selection is not active. + + Returns + ------- + altair.condition + The condition to apply to a chart. + """ return condition( f"indexof({selector.name}.{field}, '{selected}') != -1", if_true, if_false )