Skip to content

Commit

Permalink
Merge 9e9f006 into 692c029
Browse files Browse the repository at this point in the history
  • Loading branch information
twheys committed Nov 14, 2018
2 parents 692c029 + 9e9f006 commit 7943e46
Show file tree
Hide file tree
Showing 5 changed files with 303 additions and 563 deletions.
67 changes: 43 additions & 24 deletions fireant/slicer/dimensions.py
Expand Up @@ -28,11 +28,19 @@ class Dimension(SlicerElement):
:param definition:
A pypika expression which is used to select the value when building SQL queries.
:param display_definition:
A pypika expression which is used to select the display value for this dimension.
:param hyperlink_template:
A hyperlink template for constructing a URL that can link a value for a dimension to a web page. This is used
by some transformers such as the ReactTable transformer for displaying hyperlinks.
"""

def __init__(self, key, label=None, definition=None, display_definition=None):
def __init__(self, key, label=None, definition=None, display_definition=None, hyperlink_template=None):
super(Dimension, self).__init__(key, label, definition, display_definition)
self.is_rollup = False
self.hyperlink_template = hyperlink_template

@immutable
def rollup(self):
Expand All @@ -53,10 +61,11 @@ class BooleanDimension(Dimension):
value.
"""

def __init__(self, key, label=None, definition=None):
def __init__(self, key, label=None, definition=None, hyperlink_template=None):
super(BooleanDimension, self).__init__(key,
label,
definition)
label=label,
definition=definition,
hyperlink_template=hyperlink_template)

def is_(self, value: bool):
"""
Expand Down Expand Up @@ -88,7 +97,8 @@ def like(self, pattern, *patterns):
A slicer query filter used to filter a slicer query to results where this dimension's display definition
matches the pattern.
"""
return PatternFilter(getattr(self, self.pattern_definition_attribute), pattern, *patterns)
definition = getattr(self, self.pattern_definition_attribute)
return PatternFilter(definition, pattern, *patterns)

def not_like(self, pattern, *patterns):
"""
Expand All @@ -104,7 +114,8 @@ def not_like(self, pattern, *patterns):
A slicer query filter used to filter a slicer query to results where this dimension's display definition
matches the pattern.
"""
return AntiPatternFilter(getattr(self, self.pattern_definition_attribute), pattern, *patterns)
definition = getattr(self, self.pattern_definition_attribute)
return AntiPatternFilter(definition, pattern, *patterns)


class CategoricalDimension(PatternFilterableMixin, Dimension):
Expand All @@ -113,10 +124,11 @@ class CategoricalDimension(PatternFilterableMixin, Dimension):
provides support for configuring a display value for each of the possible values.
"""

def __init__(self, key, label=None, definition=None, display_values=()):
def __init__(self, key, label=None, definition=None, hyperlink_template=None, display_values=()):
super(CategoricalDimension, self).__init__(key,
label,
definition)
label=label,
definition=definition,
hyperlink_template=hyperlink_template)
self.display_values = dict(display_values)

def isin(self, values: Iterable):
Expand Down Expand Up @@ -179,11 +191,13 @@ class UniqueDimension(_UniqueDimensionBase):
This is a dimension that represents a field in a database which is a unique identifier, such as a primary/foreign
key. It provides support for a display value field which is selected and used in the results.
"""
def __init__(self, key, label=None, definition=None, display_definition=None):

def __init__(self, key, label=None, definition=None, display_definition=None, hyperlink_template=None):
super(UniqueDimension, self).__init__(key,
label,
definition,
display_definition)
label=label,
definition=definition,
display_definition=display_definition,
hyperlink_template=hyperlink_template)
if display_definition is not None:
self.display = DisplayDimension(self)

Expand All @@ -209,8 +223,9 @@ class DisplayDimension(_UniqueDimensionBase):

def __init__(self, dimension):
super(DisplayDimension, self).__init__('{}_display'.format(dimension.key),
dimension.label,
dimension.display_definition)
label=dimension.label,
definition=dimension.display_definition,
hyperlink_template=dimension.hyperlink_template)


class ContinuousDimension(Dimension):
Expand All @@ -219,10 +234,12 @@ class ContinuousDimension(Dimension):
or date/time. It requires the use of an interval which is the window over the values.
"""

def __init__(self, key, label=None, definition=None, default_interval=NumericInterval(1, 0)):
def __init__(self, key, label=None, definition=None, hyperlink_template=None,
default_interval=NumericInterval(1, 0)):
super(ContinuousDimension, self).__init__(key,
label,
definition)
label=label,
definition=definition,
hyperlink_template=hyperlink_template)
self.interval = default_interval


Expand All @@ -233,10 +250,11 @@ class DatetimeDimension(ContinuousDimension):
week-over-week or month-over-month.
"""

def __init__(self, key, label=None, definition=None, default_interval=daily):
def __init__(self, key, label=None, definition=None, hyperlink_template=None, default_interval=daily):
super(DatetimeDimension, self).__init__(key,
label,
definition,
label=label,
definition=definition,
hyperlink_template=hyperlink_template,
default_interval=default_interval)

@immutable
Expand Down Expand Up @@ -281,6 +299,7 @@ def __init__(self, dimension):
else None

super(TotalsDimension, self).__init__(dimension.key,
dimension.label,
totals_definition,
display_definition)
label=dimension.label,
definition=totals_definition,
display_definition=display_definition,
hyperlink_template=dimension.hyperlink_template)
163 changes: 123 additions & 40 deletions fireant/slicer/widgets/reacttable.py
@@ -1,3 +1,4 @@
import re
from collections import OrderedDict

import numpy as np
Expand Down Expand Up @@ -139,11 +140,15 @@ def __repr__(self):
@staticmethod
def map_display_values(df, dimensions):
"""
WRITEME
Creates a mapping for dimension values to their display values.
:param df:
The result data set that is being transformed.
:param dimensions:
The list of dimensions included in the query that created the result data set df.
:return:
A tree-structure dict with two levels of depth. The top level dict has keys for each dimension's display
key. The lower level dict has keys for each raw dimension value and values which are the display value.
"""
dimension_display_values = {}

Expand All @@ -163,6 +168,55 @@ def map_display_values(df, dimensions):

return dimension_display_values

@staticmethod
def map_hyperlink_templates(df, dimensions):
"""
Creates a mapping for each dimension to it's hyperlink template if it is possible to create the hyperlink
template for it.
The hyperlink template is a URL-like string containing curley braces enclosing dimension keys: `{dimension}`.
While rendering this widget, the dimension key placeholders need to be replaced with the dimension values for
that row.
:param df:
The result data set that is being transformed. The data frame SHOULD be pivoted/transposed if that step is
required, before calling this function, in order to prevent the template from being included for the
dimension if one of the required dimensions is pivoted.
:param dimensions:
The list of dimensions included in the query that created the result data set df.
:return:
A dict with the dimension key as the key and the hyperlink template as the value. Templates will only be
included if it will be possible to fill in the required parameters.
"""
hyperlink_templates = {}
pattern = re.compile(r'{[^{}]+}')

for dimension in dimensions:
hyperlink_template = dimension.hyperlink_template
if hyperlink_template is None:
continue

required_hyperlink_parameters = [format_dimension_key(argument[1:-1])
for argument in pattern.findall(hyperlink_template)]

# Check that all of the required dimensions are in the result data set. Only include the hyperlink template
# in the return value of this function if all are present.
unavailable_hyperlink_parameters = set(required_hyperlink_parameters) & set(df.index.names)
if not unavailable_hyperlink_parameters:
continue

# replace the dimension keys with the formatted values. This will come in handy later when replacing the
# actual values
hyperlink_template = hyperlink_template.format(**{
argument[3:]: '{' + argument + '}'
for argument in required_hyperlink_parameters
})

f_dimension_key = format_dimension_key(dimension.key)
hyperlink_templates[f_dimension_key] = hyperlink_template

return hyperlink_templates

@staticmethod
def format_data_frame(data_frame, dimensions):
"""
Expand Down Expand Up @@ -314,8 +368,56 @@ def _make_columns(columns_frame, previous_levels=()):
column_frame = data_frame.columns.to_frame()
return _make_columns(column_frame)

@staticmethod
def transform_data(data_frame, item_map, dimension_display_values):
@classmethod
def transform_data_row_index(cls, index_values, dimension_display_values, dimension_hyperlink_templates):
# Add the index to the row
row = {}
for key, value in index_values.items():
if key is None:
continue

data = {RAW_VALUE: value}

# Try to find a display value for the item. If this is a metric the raw value is replaced with the
# display value because there is no raw value for a metric label
display = getdeepattr(dimension_display_values, (key, value))
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)

row[key] = data

return row

@classmethod
def transform_data_row_values(cls, series, item_map):
# Add the values to the row
row = {}
for key, value in series.iteritems():
value = metric_value(value)
data = {RAW_VALUE: metric_value(value)}

# Try to find a display value for the item
item = item_map.get(key[0] if isinstance(key, tuple) else key)
display = metric_display(value,
getattr(item, 'prefix', None),
getattr(item, 'suffix', None),
getattr(item, 'precision', None))

if display is not None:
data['display'] = display

setdeepattr(row, key, data)

return row

@classmethod
def transform_data(cls, data_frame, item_map, dimension_display_values, dimension_hyperlink_templates):
"""
Builds a list of dicts containing the data for ReactTable. This aligns with the accessors set by
#transform_dimension_column_headers and #transform_metric_column_headers
Expand All @@ -326,8 +428,11 @@ def transform_data(data_frame, item_map, dimension_display_values):
A map to find metrics/operations based on their keys found in the data frame.
:param dimension_display_values:
A map for finding display values for dimensions based on their key and value.
:param dimension_hyperlink_templates:
"""
result = []
index_names = data_frame.index.names

rows = []

for index, series in data_frame.iterrows():
if not isinstance(index, tuple):
Expand All @@ -339,44 +444,19 @@ def transform_data(data_frame, item_map, dimension_display_values):
if item not in item_map
else getattr(item_map[item], 'label', item_map[item].key)
for item in index]
index_values = OrderedDict(zip(index_names, index))

row = {}

# Add the index to the row
for key, value in zip(data_frame.index.names, index):
if key is None:
continue
index_cols = cls.transform_data_row_index(index_values,
dimension_display_values,
dimension_hyperlink_templates)
value_cols = cls.transform_data_row_values(series, item_map)

data = {RAW_VALUE: value}

# Try to find a display value for the item. If this is a metric the raw value is replaced with the
# display value because there is no raw value for a metric label
display = getdeepattr(dimension_display_values, (key, value))
if display is not None:
data['display'] = display

row[key] = data

# Add the values to the row
for key, value in series.iteritems():
value = metric_value(value)
data = {RAW_VALUE: metric_value(value)}

# Try to find a display value for the item
item = item_map.get(key[0] if isinstance(key, tuple) else key)
display = metric_display(value,
getattr(item, 'prefix', None),
getattr(item, 'suffix', None),
getattr(item, 'precision', None))

if display is not None:
data['display'] = display

setdeepattr(row, key, data)

result.append(row)
row = {}
row.update(index_cols)
row.update(value_cols)
rows.append(row)

return result
return rows

def transform(self, data_frame, slicer, dimensions, references):
"""
Expand Down Expand Up @@ -409,16 +489,19 @@ def transform(self, data_frame, slicer, dimensions, references):
df = data_frame[df_dimension_columns + df_metric_columns].copy()

dimension_display_values = self.map_display_values(df, dimensions)

self.format_data_frame(df, dimensions)

dimension_keys = [format_dimension_key(dimension.key) for dimension in self.pivot]
df = self.pivot_data_frame(df, dimension_keys, self.transpose) \
.fillna(value=NAN_VALUE) \
.replace([np.inf, -np.inf], INF_VALUE)

dimension_hyperlink_templates = self.map_hyperlink_templates(df, dimensions)

dimension_columns = self.transform_dimension_column_headers(df, dimensions)
metric_columns = self.transform_metric_column_headers(df, item_map, dimension_display_values)
data = self.transform_data(df, item_map, dimension_display_values)
data = self.transform_data(df, item_map, dimension_display_values, dimension_hyperlink_templates)

return {
'columns': dimension_columns + metric_columns,
Expand Down
6 changes: 6 additions & 0 deletions fireant/tests/slicer/mocks.py
Expand Up @@ -269,6 +269,12 @@ def PoliticsRow(timestamp, candidate, candidate_display, political_party, electi
.groupby(fd('political_party')) \
.sum()

cat_uni_dim_df = mock_politics_database[[fd('political_party'), fd('candidate'), fd('candidate_display'),
fm('votes'), fm('wins')]] \
.groupby([fd('political_party'), fd('candidate'), fd('candidate_display')]) \
.sum() \
.reset_index(fd('candidate_display'))

uni_dim_df = mock_politics_database[[fd('candidate'), fd('candidate_display'), fm('votes'), fm('wins')]] \
.groupby([fd('candidate'), fd('candidate_display')]) \
.sum() \
Expand Down

0 comments on commit 7943e46

Please sign in to comment.