diff --git a/curvesim/plot/altair/chart_properties.py b/curvesim/plot/altair/chart_properties.py index d7ca6fdef..647d7925a 100644 --- a/curvesim/plot/altair/chart_properties.py +++ b/curvesim/plot/altair/chart_properties.py @@ -1,3 +1,21 @@ +""" +This module provides functionality for creating and manipulating chart properties. + +It contains utility functions for creating keyword arguments for a chart, updating +and initializing properties, and handling property-specific behaviors. These +functions support nested properties and allow for different behaviors based on +property keys. + +The key functions in this module are: + +- make_chart_kwargs: Create the keyword arguments for a chart by updating a default + dictionary with values from override dictionaries. +- update_properties: Update properties in a dictionary, supporting nested properties. +- concat_properties: Concatenate values to a property in a dictionary. +- ignore_property: Ignore a property during an update operation. +- init_properties: Initialize the properties of a dictionary using a mapping of + classes. +""" from copy import deepcopy from functools import partial @@ -5,6 +23,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 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 +51,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 + (or the count), 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 +79,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 +101,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..c18695b95 100644 --- a/curvesim/plot/altair/make_chart.py +++ b/curvesim/plot/altair/make_chart.py @@ -1,14 +1,55 @@ -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. + +The key functions in this module are: + +- make_chart: Create an interactive chart with custom properties and styles. +- make_defaults: Create default properties for a chart. + +The data_transformers.disable_max_rows() call at the top of the module disables the +maximum row limit for Altair charts. +""" +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: @@ -27,6 +68,30 @@ def make_chart(config, x=None, y=None, color=None, **kwargs): def make_defaults(title, x, y, color): + """ + 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/make_page.py b/curvesim/plot/altair/results/make_page.py index e221df23e..d8000a4fb 100644 --- a/curvesim/plot/altair/results/make_page.py +++ b/curvesim/plot/altair/results/make_page.py @@ -1,3 +1,25 @@ +""" +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. + +The key functions in this module are: + +- make_page_from_results: Creates an interactive Altair chart from simulation + results. +- make_data_dict: Prepares the data from the results for chart creation. +- make_page: Creates an interactive Altair chart page with multiple subplots. +- get_metric_data: Retrieves the relevant data for a specific metric. +- make_subplot: Creates a subplot for a specific metric. +- make_single_chart: Creates a single chart for a specific metric. +- make_layered_chart: Creates a layered chart for multiple submetrics of a metric. + +The data_transformers.disable_max_rows() call at the top of the module disables the +maximum row limit for Altair charts. +""" from altair import concat, data_transformers, layer, value, vconcat from ..make_chart import make_chart @@ -10,6 +32,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 +61,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 +122,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 +155,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 +185,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 +213,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..0ae8f133c 100644 --- a/curvesim/plot/altair/results/preprocessing.py +++ b/curvesim/plot/altair/results/preprocessing.py @@ -1,9 +1,42 @@ +""" +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. + +The key functions in this module are: + +- preprocess_data: Preprocesses data for chart creation, with an option to downsample. +- downsample_data: Downscales the data according to provided configurations and factors. +- resample_data: Resamples the data based on the provided factors and resampling functions. +- to_histograms: Converts the data to histograms. +- make_histogram: Creates a histogram from the data. +""" 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 +44,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 +89,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..abb8988b6 100644 --- a/curvesim/plot/altair/results/result_selectors.py +++ b/curvesim/plot/altair/results/result_selectors.py @@ -1,16 +1,39 @@ -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. + +The key functions in this module are: + +- make_result_selectors: Creates result selectors for an Altair chart. +- make_axis_selectors: Creates axis selectors. +- make_parameter_filters: Creates parameter filters. +- format_selector_charts: Formats selector charts for display. +""" + +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 + The result selectors and the charts to display them. + """ dynamic_axes = dict(list(dynamic_axes.items())[: len(factors)]) axes = make_axis_selectors(factors, dynamic_axes) @@ -30,6 +53,21 @@ def make_result_selectors(factors, dynamic_axes): def make_axis_selectors(factors, dynamic_axes): + """ + Creates axis selectors. + + Parameters + ---------- + factors : dict + The factors to create selectors for. + 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 +82,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 to create selectors for. + + Returns + ------- + dict + The parameter filters and the charts to display them. + """ n_dynamic_axes = len(dynamic_axes) charts = [] @@ -59,6 +112,23 @@ 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 + The index of the initial selection. + + Returns + ------- + altair.Chart, altair.CalculateTransform + The axis selector chart and the transform for the selector. + """ chart, selector = make_selector( axis, options, @@ -73,6 +143,23 @@ 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 + The index of the initial selection. + + 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 +174,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/selectors.py b/curvesim/plot/altair/selectors.py index bc4aea654..389a5000c 100644 --- a/curvesim/plot/altair/selectors.py +++ b/curvesim/plot/altair/selectors.py @@ -1,7 +1,23 @@ +""" +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. + +The key functions in this module are: + +- make_selector: Create an interactive selector chart and a selector. +- get_initial_selection: Get the initial selection value based on provided options and + an index or "all". +- make_selector_chart: Create the selector chart. +- if_selected: Apply a property if a selection is active. +""" 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 +31,39 @@ def make_selector( toggle=True, sel_idx=None, ): + """ + Create an interactive selector chart and a selector. + + 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 selector. + + Raises + ------ + PlotError + If sel_idx is not None, an integer, or 'all'. + """ title = title or "" labels = labels or options @@ -29,6 +78,30 @@ def make_selector( def get_initial_selection(field, options, sel_idx): + """ + Get the initial selection value based on provided options and an index or 'all'. + + 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 +118,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 +163,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 )