def _make_columns(columns_frame, previous_level_values=()): """ This function recursively creates the individual column definitions for React Table with the above tree structure depending on how many index levels there are in the columns. :param columns_frame: A data frame representing the columns of the result set data frame. :param previous_level_values: A tuple containing the higher level index level values used for building the data accessor path """ f_dimension_alias = columns_frame.index.names[0] # Group the columns if they are multi-index so we can get the proper sub-column values. This will yield # one group per dimension value with the group data frame containing only the relevant sub-columns groups = (columns_frame.groupby( level=0) if isinstance(columns_frame.index, pd.MultiIndex) else [(level, None) for level in columns_frame.index]) columns = [] for column_value, group in groups: if column_value in hide_metric_aliases: continue is_totals = column_value in TOTALS_MARKERS | {TOTALS_LABEL} # All column definitions have a header column = { "Header": get_header(column_value, f_dimension_alias, is_totals) } level_values = previous_level_values + (column_value, ) if group is not None: # If there is a group, then drop this index level from the group data frame and recurse to build # sub column definitions next_level_df = group.reset_index(level=0, drop=True) column["columns"] = _make_columns(next_level_df, level_values) else: column["accessor"] = ".".join( safe_value(value) for value in level_values) # path_accessor can be useful for front-end code e.g. if specifying a custom accessor function. # Use cases for this include if the value of the nested column name includes a '.' character, # which breaks the default nested column access functionality. column["path_accessor"] = [ safe_value(value) for value in level_values ] if is_totals: column["className"] = "fireant-totals" columns.append(column) return columns
def get_header(column_value, f_dimension_alias, is_totals): if f_dimension_alias == F_METRICS_DIMENSION_ALIAS or is_totals: item = field_map[column_value] return getattr(item, "label", item.alias) if f_dimension_alias in field_map: field = field_map[f_dimension_alias] return _display_value(column_value, field) or safe_value(column_value) if f_dimension_alias is None: return "" return safe_value(column_value)
def transform_row_values(series, fields, is_transposed): # Add the values to the row index_names = series.index.names or [] row = {} for key, value in series.iteritems(): key = wrap_list(key) # Get the field for the metric metric_alias = wrap_list( series.name)[0] if is_transposed else key[0] field = fields[metric_alias] data = {RAW_VALUE: raw_value(value, field)} display = _display_value(value, field, date_as=return_none) if display is not None: data["display"] = display accessor_fields = [ fields[field_alias] for field_alias in index_names if field_alias is not None ] accessor = [ safe_value(value) for value, field in zip(key, accessor_fields) ] or key setdeepattr(row, accessor, data) return row
def transform_row_index(index_values, field_map, dimension_hyperlink_templates): # Add the index to the row row = {} for key, value in index_values.items(): if key is None: continue field_alias = key field = field_map[field_alias] data = {RAW_VALUE: raw_value(value, field)} display = _display_value(value, field) if display is not None: data["display"] = display # If the dimension has a hyperlink template, then apply the template by formatting it with the dimension # values for this row. The values contained in `index_values` will always contain all of the required values # at this point, otherwise the hyperlink template will not be included. if key in dimension_hyperlink_templates: data["hyperlink"] = dimension_hyperlink_templates[key].format( **index_values) safe_key = safe_value(key) row[safe_key] = data return row
def transform_index_column_headers(data_frame, field_map): """ Convert the un-pivoted dimensions into ReactTable column header definitions. :param data_frame: The result set data frame :param field_map: :return: A list of column header definitions with the following structure. .. code-block:: jsx columns = [{ Header: 'Column A', accessor: 'a', }, { Header: 'Column B', accessor: 'b', }] """ columns = [] if (not isinstance(data_frame.index, pd.MultiIndex) and data_frame.index.name is None): return columns for f_dimension_alias in data_frame.index.names: dimension = field_map[f_dimension_alias] header = getattr(dimension, "label", dimension.alias) columns.append({ "Header": json_value(header), "accessor": safe_value(f_dimension_alias), }) return columns
def test_decimal_value_is_returned_with_decimal_point_replaced(self): tests = [ (0.0, "0$0"), (-1.1, "-1$1"), (1.1, "1$1"), (0.123456789, "0$123456789"), (-0.123456789, "-0$123456789"), ] for value, expected in tests: with self.subTest("using value" + str(value)): self.assertEqual(expected, formats.safe_value(value))
def _get_row_value_accessor(series, fields, key): index_names = series.index.names or [] accessor_fields = [ fields[field_alias] for field_alias in index_names if field_alias is not None ] accessor = [ safe_value(value) for value, field in zip(key, accessor_fields) ] or key return accessor
def test_string_value_is_returned_with_only_safe_characters_replaced(self): tests = [ ("abcdefghijklmnopqrstuvwxyz", "abcdefghijklmnopqrstuvwxyz"), ("ABCDEFGHIJKLMNOPQRSTUVWXYZ", "ABCDEFGHIJKLMNOPQRSTUVWXYZ"), (" ", " "), ("-0123456789", "-0123456789"), (".[]", "$$$"), ("a.1", "a$1"), ("b[0]", "b$0$"), ] for value, expected in tests: with self.subTest("using value" + value): self.assertEqual(expected, formats.safe_value(value))
def transform_row_index( index_values, field_map: Dict[str, Field], dimension_hyperlink_templates: Dict[str, str], hide_dimension_aliases: List[str], row_colors: List[str], ): # Add the index to the row row = {} for key, value in index_values.items(): if key is None or key not in field_map: continue field_alias = key field = field_map[field_alias] data = {RAW_VALUE: raw_value(value, field)} display = _display_value(value, field) if display is not None: data["display"] = display if row_colors is not None: data["color"], data["text_color"] = row_colors is_totals = display == TOTALS_LABEL # If the dimension has a hyperlink template, then apply the template by formatting it with the dimension # values for this row. The values contained in `index_values` will always contain all of the required values # at this point, otherwise the hyperlink template will not be included. if not is_totals and key in dimension_hyperlink_templates: try: data["hyperlink"] = dimension_hyperlink_templates[ key].format(**index_values) except KeyError: pass safe_key = safe_value(key) row[safe_key] = data for dimension_alias in hide_dimension_aliases: if dimension_alias in row: del row[dimension_alias] return row
def transform_index_column_headers( data_frame: pd.DataFrame, field_map: Dict[str, Field], hide_dimension_aliases: List[str]) -> List[dict]: """ Convert the un-pivoted dimensions into ReactTable column header definitions. :param data_frame: The result set data frame. :param field_map: A map to find metrics/operations based on their keys found in the data frame. :param hide_dimension_aliases: A set with hide dimension aliases. :return: A list of column header definitions with the following structure. .. code-block:: jsx columns = [{ Header: 'Column A', accessor: 'a', }, { Header: 'Column B', accessor: 'b', }] """ columns = [] if not isinstance(data_frame.index, pd.MultiIndex) and data_frame.index.name is None: return columns for f_dimension_alias in data_frame.index.names: if f_dimension_alias not in field_map or f_dimension_alias in hide_dimension_aliases: continue dimension = field_map[f_dimension_alias] header = getattr(dimension, "label", dimension.alias) columns.append({ "Header": json_value(header), "accessor": safe_value(f_dimension_alias), }) return columns
def test_inf_returned_as_inf_label(self): with self.subTest("positive inf"): self.assertEqual("inf", formats.safe_value(np.inf)) with self.subTest("negative inf"): self.assertEqual("inf", formats.safe_value(-np.inf))
def test_nan_returned_as_null_label(self): self.assertEqual("null", formats.safe_value(np.nan))
def test_none_returned_as_null_label(self): self.assertEqual("null", formats.safe_value(None))