diff --git a/altair/utils/schemapi.py b/altair/utils/schemapi.py index 14da63cb0..4db4b238d 100644 --- a/altair/utils/schemapi.py +++ b/altair/utils/schemapi.py @@ -5,6 +5,7 @@ import inspect import json import textwrap +from itertools import zip_longest import jsonschema import numpy as np @@ -116,30 +117,83 @@ def __str__(self): for val in schema_path[:-1] if val not in ("properties", "additionalProperties", "patternProperties") ) + if hasattr(vegalite, schema_path.split(".")[-1]): + altair_class = "altair." + schema_path.split(".")[-1] vegalite_core_class = getattr(vegalite, schema_path.split(".")[-1]) param_dict_keys = inspect.signature(vegalite_core_class).parameters.keys() - param_names = ( - "Existing parameter names are: " - + str([name for name in param_dict_keys if name != "kwds"])[1:-1] - ) - altair_class = "altair." + schema_path.split(".")[-1] - return """Invalid specification - - {}, validating {!r} - {} has no parameter named {!r} - - {} - - See the help for {} to read the full description of these parameters - """.format( - schema_path, - self.validator, - altair_class, - list(self.instance.keys())[0], - textwrap.fill(param_names, subsequent_indent=" " * 12, width=80), - altair_class, + # Everything from here until the return statement is to format + # param names into a table so that they are easier to read + param_names, name_lengths = zip( + *[(name, len(name)) for name in param_dict_keys if name != "kwds"] + ) + # Worst case scenario with the same longest param name in the same + # row for all columns + max_name_length = max(name_lengths) + max_column_width = 80 + # Output a square table if not too big (since it is easier to read) + num_param_names = len(param_names) + square_columns = int(np.ceil(num_param_names**0.5)) + columns = min(max_column_width // max_name_length, square_columns) + # Compute roughly equal column heights to evenly divide the param names + def split_into_equal_parts(n, p): + return [n // p + 1] * (n % p) + [n // p] * (p - n % p) + + column_heights = split_into_equal_parts(num_param_names, columns) + + # Section the param names into columns and compute their widths + param_names_columns = [] + column_max_widths = [] + last_end_idx = 0 + for ch in column_heights: + param_names_columns.append( + param_names[last_end_idx : last_end_idx + ch] + ) + column_max_widths.append( + max([len(param_name) for param_name in param_names_columns[-1]]) + ) + last_end_idx = ch + last_end_idx + + # Transpose the param name columns into rows to facilitate looping + param_names_rows = [] + for li in zip_longest(*param_names_columns, fillvalue=""): + param_names_rows.append(li) + # Build the table as a string by iterating over and formatting the rows + param_names_table = "" + for param_names_row in param_names_rows: + for num, param_name in enumerate(param_names_row): + # Set column width based on the longest param in the column + max_name_length_column = column_max_widths[num] + column_pad = 3 + param_names_table += "{:<{}}".format( + param_name, max_name_length_column + column_pad + ) + # Insert newlines and spacing after the last element in each row + if num == (len(param_names_row) - 1): + param_names_table += "\n" + # 16 is the indendation of the returned multiline string below + param_names_table += " " * 16 + + # cleandoc removes multiline string indentation in the output + return inspect.cleandoc( + """Invalid specification + + {}, validating {!r} + + {} has no parameter named {!r} + + Existing parameter names are: + {} + See the help for {} to read the full description of these parameters + """.format( + schema_path, + self.validator, + altair_class, + list(self.instance.keys())[0], + param_names_table, + altair_class, + ) ) # Fall back on the less informative error message else: